using System; using System.Runtime.InteropServices; using System.Collections.Generic; using UdonSharp; using UnityEngine; using VRC.SDK3.Rendering; using VRC.Udon.Common.Interfaces; public class DataDecoder : UdonSharpBehaviour { public RenderTexture sourceTexture; public int tileToCheck = 0; private int tileSize = 8; // Minimum size (in pixels) of a tile. This is shared with our tixl operator. private const int kMinTileSize = 4; private const int kMaxTileSize = 128; private Color32[] pixelData; private bool hasData = false; private int readWidth; private int readHeight; // Top-level data types. private const int kT_TimeSyncData = 0; void Start() {} void Update() { if (sourceTexture == null) return; // TODO only request as much as we need.... int requestWidth = sourceTexture.width; int requestHeight = sourceTexture.height; int pixelCount = requestWidth * requestHeight; if (pixelCount <= 0) return; if (pixelData == null || pixelCount != pixelData.Length) { pixelData = new Color32[pixelCount]; hasData = false; } readWidth = requestWidth; readHeight = requestHeight; VRCAsyncGPUReadback.Request(sourceTexture, 0, 0, readWidth, 0, readHeight, 0, 1, (IUdonEventReceiver)this); if (hasData) { ProcessTiles(); hasData = false; } } public override void OnAsyncGpuReadbackComplete(VRCAsyncGPUReadbackRequest request) { if (request.hasError) return; if (pixelData != null && request.TryGetData(pixelData)) { hasData = true; } } // Byte parsing logic private bool HasBytesLeft(ref byte[] b, int offset, int bytesLeft) { return b.Length - offset >= bytesLeft; } private int GetInt(ref byte[] b, ref int offset) { int ret = BitConverter.ToInt32(b, offset); offset += 4; return ret; } private float GetFloat(ref byte[] b, ref int offset) { float ret = BitConverter.ToSingle(b, offset); offset += 4; return ret; } private void ProcessTiles() { // Get the tile size. { int oldTileSize = tileSize; tileSize = kMinTileSize; tileSize = Parse24BitTile(0); tileSize = Mathf.Clamp(tileSize, kMinTileSize, kMaxTileSize); if (tileSize != oldTileSize) { Debug.Log($"Tile size changed from {oldTileSize} to {tileSize}"); } } // Get the length. This is in units of subpixels. So we will need to access // ceil(length/3) tiles. int lengthSubpixels = Parse24BitTile(1); int lengthTiles = (int) Mathf.Ceil(lengthSubpixels/3.0f); Color32 parsed_first = GetTileRGB(0); Debug.Log($"First tile: {parsed_first.r} {parsed_first.g} {parsed_first.b}"); Debug.Log($"Parsed size {tileSize}"); Debug.Log($"Parsed length {lengthSubpixels}"); // Collect all nibbles into a flat array. Note that these are still // encoded. int[] nibbles = new int[lengthSubpixels]; int nibbleCount = 0; for (int tile_i = 0; tile_i < lengthTiles; tile_i++) { Color32 parsed_i = GetTileRGB(tile_i+2); nibbles[nibbleCount++] = parsed_i.r; if (nibbleCount < lengthSubpixels) { nibbles[nibbleCount++] = parsed_i.g; } if (nibbleCount < lengthSubpixels) { nibbles[nibbleCount++] = parsed_i.b; } } // Convert nibbles to bytes. int byteCount = nibbleCount / 2; byte[] bytes = new byte[byteCount]; for (int i = 0; i < byteCount; i++) { // See DataEncoder.cs. It puts the upper 4 bits before the lower 4 bits. bytes[i] = (byte) ((nibbles[2*i] & 0xF0) | ((nibbles[2*i+1] & 0xF0) >> 4)); } Debug.Log($"Parsed {bytes.Length} bytes from {nibbles.Length} subpixels"); int bOff = 0; while (HasBytesLeft(ref bytes, bOff, 8)) { int type = GetInt(ref bytes, ref bOff); int length = GetInt(ref bytes, ref bOff); switch (type) { case kT_TimeSyncData: { if (!HasBytesLeft(ref bytes, bOff, 8)) { break; } float lastSyncTimeMs = GetFloat(ref bytes, ref bOff); float measureTimeUs = GetFloat(ref bytes, ref bOff); Debug.Log($"Parsed time sync data: {lastSyncTimeMs} {measureTimeUs}"); break; } } } int tilesPerColumn = (int) Mathf.Floor(readHeight / tileSize); } private int Parse24BitTile(int tileIdx) { Color32 parsed = GetTileRGB(tileIdx); int data = 0; data |= DecodeNibble(parsed.r); data |= DecodeNibble(parsed.g) << 4; data |= DecodeNibble(parsed.b) << 8; return data; } private int DecodeNibble(int subpixel) { return (subpixel >> 4) & 0x0F; } private Color32 GetTileRGB(int tileIdx) { int tileY = tileIdx * tileSize; int centerY = tileY + tileSize / 2; if (centerY >= readHeight) return new Color32(); int localX = tileSize / 2; int localY = readHeight - 1 - centerY; int index = localY * readWidth + localX; if (index < 0 || index >= pixelData.Length) return new Color32(); return pixelData[index]; } }