using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;
using VRC.SDK3.Rendering;
using VRC.Udon.Common.Interfaces;
public class DataDecoder : UdonSharpBehaviour
{
public RenderTexture sourceTexture;
public int tileSize = 8;
private const int MaxTiles = 8192;
private bool isValid;
private Color[] pixelDataFloat;
private Color32[] pixelDataBytes;
private bool hasData = false;
private bool hasReportedTileLimit;
private const int PixelFormatNone = 0;
private const int PixelFormatColor32 = 1;
private const int PixelFormatFloat = 2;
private int currentPixelFormat = PixelFormatNone;
void Start() {}
private bool IsValid()
{
if (sourceTexture == null) {
Debug.LogWarning("DataDecoder: sourceTexture missing");
return false;
}
if (tileSize <= 0) {
Debug.LogWarning("DataDecoder: invalid tile size");
return false;
}
return true;
}
void Update()
{
// Only call IsValid() until it succeeds, then cache.
if (!isValid)
{
isValid = IsValid();
if (!isValid) {
return;
}
}
int pixelCount = sourceTexture.width * sourceTexture.height;
if (pixelDataBytes == null || pixelCount != pixelDataBytes.Length)
{
Debug.Log($"DataDecoder: allocating Color32 buffer for {pixelCount} pixels");
pixelDataBytes = new Color32[pixelCount];
}
if (pixelDataFloat == null || pixelCount != pixelDataFloat.Length)
{
pixelDataFloat = new Color[pixelCount];
}
// Request readback every frame (multiple requests can be in flight)
VRCAsyncGPUReadback.Request(sourceTexture, 0, (IUdonEventReceiver)this);
// Process data if available
if (hasData)
{
DecodeData(tileSize);
hasData = false;
}
}
public override void OnAsyncGpuReadbackComplete(VRCAsyncGPUReadbackRequest request)
{
if (request.hasError)
{
Debug.LogError("DataDecoder: GPU readback error");
return;
}
// Store the pixel data (overwrites previous data with latest)
if (pixelDataBytes != null && request.TryGetData(pixelDataBytes))
{
currentPixelFormat = PixelFormatColor32;
hasData = true;
return;
}
if (pixelDataFloat != null && request.TryGetData(pixelDataFloat))
{
currentPixelFormat = PixelFormatFloat;
hasData = true;
return;
}
Debug.LogWarning("DataDecoder: Unable to read GPU data into any supported buffer format");
}
///
/// Prints the RGB values of the first 4 tiles for debugging purposes.
///
private void PrintFirst4Tiles(int tileSize, int tilesPerColumn)
{
int tilesPerRow = (sourceTexture.width + tileSize - 1) / tileSize;
int totalTiles = Mathf.Max(Mathf.Min(tilesPerColumn * tilesPerRow, MaxTiles), 0);
if (totalTiles == 0)
{
Debug.Log("DataDecoder: tiles=");
return;
}
int tilesToLog = Mathf.Min(4, totalTiles);
string log = "DataDecoder: tiles=";
for (int tileIdx = 0; tileIdx < tilesToLog; tileIdx++)
{
GetTileRGB(tileIdx, tileSize, tilesPerColumn, out byte r, out byte g, out byte b);
log += $"[{tileIdx}:{r},{g},{b}]";
if (tileIdx < tilesToLog - 1)
{
log += " ";
}
}
if (totalTiles > tilesToLog)
{
log += " ...";
}
Debug.Log(log);
}
///
/// Decodes data from a texture where the data is encoded in RGB channels of tiles.
/// The first tile contains the length (number of bytes to decode).
/// Subsequent tiles contain the actual data.
///
/// The size of each tile (e.g., 8 for 8x8 tiles)
/// An array of decoded bytes
public byte[] DecodeData(int tileSize)
{
if (!HasPixelData())
{
Debug.LogWarning("DataDecoder: No pixel data available");
return new byte[0];
}
int tilesPerColumn = (sourceTexture.height + tileSize - 1) / tileSize;
int tilesPerRow = (sourceTexture.width + tileSize - 1) / tileSize;
PrintFirst4Tiles(tileSize, tilesPerColumn);
// Read the length from the first tile
int length = ReadLength(tileSize, tilesPerColumn);
//Debug.Log($"Parsed length: {length}");
if (length <= 0) {
Debug.LogWarning($"DataDecoder: Length is 0 or negative: {length}");
return new byte[0];
}
// Calculate how many data tiles we need (excluding the length tile)
int bytesPerTile = 3; // RGB from center pixel only
int tilesNeeded = (length + bytesPerTile - 1) / bytesPerTile; // Ceiling division
// Allocate result array
byte[] data = new byte[length];
// Calculate tiles per column
// tilesPerColumn/Row calculated above
if (tilesPerColumn <= 0 || tilesPerRow <= 0)
{
Debug.LogError("DataDecoder: Texture dimensions smaller than tile size");
return new byte[0];
}
int totalTilesRaw = tilesPerColumn * tilesPerRow;
int totalTiles = Mathf.Min(totalTilesRaw, MaxTiles);
if (totalTilesRaw <= MaxTiles)
{
hasReportedTileLimit = false;
}
else if (!hasReportedTileLimit)
{
Debug.LogWarning($"DataDecoder: Limiting decoding to first {MaxTiles} tiles (of {totalTilesRaw})");
hasReportedTileLimit = true;
}
// Read data from each tile (starting after the length tile)
int byteIndex = 0;
for (int tileIdx = 0; tileIdx < tilesNeeded && byteIndex < length; tileIdx++)
{
// Skip tile 0 (the length tile) by using actualTileIdx
int actualTileIdx = tileIdx + 1;
if (actualTileIdx >= totalTiles)
{
Debug.LogWarning($"DataDecoder: Ran out of tiles at index {actualTileIdx} while decoding {length} bytes");
break;
}
// Get RGB bytes from this tile
GetTileRGB(actualTileIdx, tileSize, tilesPerColumn, out byte r, out byte g, out byte b);
// Write bytes to output array
if (byteIndex < length) data[byteIndex++] = r;
if (byteIndex < length) data[byteIndex++] = g;
if (byteIndex < length) data[byteIndex++] = b;
}
return data;
}
///
/// Gets the RGB bytes from the center pixel of the nth tile.
/// Tiles are indexed in column-major order (tile 0 is top-left).
///
/// The index of the tile (0-based)
/// The size of each tile
/// Output: Red channel as byte (0-255)
/// Output: Green channel as byte (0-255)
/// Output: Blue channel as byte (0-255)
/// The center pixel color, or Color.clear if invalid data detected
private Color GetTileRGB(int tileIndex, int tileSize, int tilesPerColumn, out byte r, out byte g, out byte b)
{
// Calculate tile position (column-major order)
int columnIdx = tileIndex / tilesPerColumn;
int rowIdx = tileIndex % tilesPerColumn;
int tileX = columnIdx * tileSize;
int tileY = rowIdx * tileSize;
int xStart = tileX;
int xEnd = Mathf.Min(tileX + tileSize, sourceTexture.width);
int yStart = tileY;
int yEnd = Mathf.Min(tileY + tileSize, sourceTexture.height);
if (xStart >= xEnd || yStart >= yEnd)
{
r = 0;
g = 0;
b = 0;
return Color.clear;
}
// Sample roughly from the tile center, clamped to the valid area.
int centerX = Mathf.Clamp(tileX + tileSize / 2, xStart, xEnd - 1);
int centerY = Mathf.Clamp(tileY + tileSize / 2, yStart, yEnd - 1);
int sampleY = Mathf.Clamp(sourceTexture.height - 1 - centerY, 0, sourceTexture.height - 1);
int index = sampleY * sourceTexture.width + centerX;
switch (currentPixelFormat)
{
case PixelFormatColor32:
{
Color32 c32 = pixelDataBytes[index];
r = c32.r;
g = c32.g;
b = c32.b;
return new Color(c32.r / 255f, c32.g / 255f, c32.b / 255f, c32.a / 255f);
}
case PixelFormatFloat:
{
Color pixel = pixelDataFloat[index];
// Check for invalid data
if (float.IsNaN(pixel.r) || float.IsInfinity(pixel.r) ||
float.IsNaN(pixel.g) || float.IsInfinity(pixel.g) ||
float.IsNaN(pixel.b) || float.IsInfinity(pixel.b))
{
Debug.LogWarning($"DataDecoder: Invalid pixel data detected in tile {tileIndex}: {pixel}");
r = 0;
g = 0;
b = 0;
return Color.clear;
}
// Convert float (0-1) to bytes (0-255), clamping to prevent overflow
r = FloatToByte(pixel.r);
g = FloatToByte(pixel.g);
b = FloatToByte(pixel.b);
return pixel;
}
default:
r = 0;
g = 0;
b = 0;
return Color.clear;
}
}
///
/// Reads the length value from the center pixel of the first tile (at position 0, 0).
/// The length is stored as a 24-bit integer (3 bytes) in the RGB channels.
///
private int ReadLength(int tileSize, int tilesPerColumn)
{
// Get RGB bytes from the first tile (tile 0)
// GetTileRGB handles NaN/infinity checks internally
GetTileRGB(0, tileSize, tilesPerColumn, out byte r, out byte g, out byte b);
// Convert bytes to int (little-endian, 24-bit)
int length = r | (g << 8) | (b << 16);
return length;
}
private bool HasPixelData()
{
switch (currentPixelFormat)
{
case PixelFormatColor32:
return pixelDataBytes != null && pixelDataBytes.Length > 0;
case PixelFormatFloat:
return pixelDataFloat != null && pixelDataFloat.Length > 0;
default:
return false;
}
}
private byte FloatToByte(float value)
{
return (byte)Mathf.Clamp(Mathf.RoundToInt(value * 255f), 0, 255);
}
}