Decision: Always Use Explicit GraphicsFormat for RenderTextures
October 26, 2025 · View on GitHub
Date: 2025-10-05 Status: ✅ Implemented Decision Maker: Bug investigation (TYPELESS texture format issue) Impacts: All RenderTextures with enableRandomWrite, GPU compute shader pipelines
Decision Summary
We always use explicit GraphicsFormat via RenderTextureDescriptor when creating RenderTextures, especially those with enableRandomWrite = true.
Core Rules:
- ✅ ALWAYS use
RenderTextureDescriptorwith explicitGraphicsFormatenum - ✅ ALWAYS verify format in RenderDoc when debugging GPU issues
- ❌ NEVER rely on
RenderTextureFormatalone when usingenableRandomWrite - ❌ NEVER assume Unity will pick the correct format automatically
Context & Problem
The TYPELESS Format Trap
Symptom: ~1000 provinces showing gray (ocean color RGB 123,169,231) instead of country colors, despite:
- Valid RGB data in provinces.bmp
- Correct packed values in CPU Color32 array
- ProvinceIDTexture containing correct data (confirmed via ReadPixels)
RenderDoc Discovery:
ProvinceIDTexture: Format DXGI_FORMAT_R8G8B8A8_TYPELESS
Problem: TYPELESS format means GPU doesn't know how to interpret byte values:
- Same bytes can be interpreted as uint8, int8, float, or snorm
- Shader reads garbage without proper interpretation
- Platform-dependent behavior (works on some platforms, fails on others)
Investigation Journey
What We Tried
Attempt 1: Add unorm Shader Qualifier
// Compute shader
RWTexture2D<unorm float4> ProvinceIDTexture;
❌ Failed - Shader type qualifiers don't override RenderTexture format
Attempt 2: Use Different RenderTextureFormat Enum
new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32);
❌ Failed - Still became TYPELESS when enableRandomWrite = true
Attempt 3: Explicit GraphicsFormat
var descriptor = new RenderTextureDescriptor(width, height,
UnityEngine.Experimental.Rendering.GraphicsFormat.R8G8B8A8_UNorm, 0);
descriptor.enableRandomWrite = true;
var texture = new RenderTexture(descriptor);
✅ Success - Format stayed R8G8B8A8_UNorm
The Decision
Chosen Approach: Explicit GraphicsFormat
Always use RenderTextureDescriptor with explicit GraphicsFormat for UAV-enabled textures:
// WRONG - May become TYPELESS on some platforms
var texture = new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32);
texture.enableRandomWrite = true; // ❌ Triggers TYPELESS on some platforms
// RIGHT - Explicit format guaranteed
var descriptor = new RenderTextureDescriptor(
width,
height,
UnityEngine.Experimental.Rendering.GraphicsFormat.R8G8B8A8_UNorm, // ✅ Explicit
0 // No depth buffer
);
descriptor.enableRandomWrite = true;
var texture = new RenderTexture(descriptor);
Why This Happens
Unity's Format Selection Logic
Without explicit GraphicsFormat:
- Unity sees
RenderTextureFormat.ARGB32 - Unity sees
enableRandomWrite = true(UAV requirement) - Unity checks platform support for R8G8B8A8_UNorm with UAV
- On some platforms: Creates TYPELESS format as "compatible" alternative
- Result: GPU doesn't know how to interpret bytes
With explicit GraphicsFormat:
- Unity sees
GraphicsFormat.R8G8B8A8_UNorm(explicit request) - Unity creates EXACTLY that format
- Result: GPU interprets bytes as unsigned normalized [0,1]
Platform Differences
TYPELESS is platform-dependent:
- Some platforms: R8G8B8A8_UNorm works fine with UAV
- Other platforms: Fallback to TYPELESS when UAV requested
- Result: Code works in editor, fails in builds (or vice versa)
Explicit format prevents this:
- Forces Unity to create requested format
- Errors clearly if platform doesn't support it
- Consistent behavior across platforms
Rationale
Why Explicit GraphicsFormat is Better
1. Deterministic Behavior
- Same format on all platforms
- Critical for multiplayer (GPU state must match)
- No platform-dependent surprises
2. Clear Error Messages
- If platform doesn't support format, Unity errors immediately
- Better than silent TYPELESS fallback that causes rendering bugs
3. Future-Proof
- Unity is moving toward GraphicsFormat API
- RenderTextureFormat is legacy
- Explicit format is the modern approach
4. Debugging Clarity
- RenderDoc shows exactly what you requested
- No guessing about format interpretation
- Matches shader expectations
Implementation Pattern
Standard RenderTexture Creation
/// <summary>
/// Create RenderTexture with guaranteed format
/// </summary>
private RenderTexture CreateProvinceIDTexture(int width, int height)
{
// Use RenderTextureDescriptor with explicit GraphicsFormat
var descriptor = new RenderTextureDescriptor(
width,
height,
UnityEngine.Experimental.Rendering.GraphicsFormat.R8G8B8A8_UNorm,
0 // No depth buffer
);
// Configure UAV support
descriptor.enableRandomWrite = true; // Required for compute shader writes
descriptor.useMipMap = false; // No mipmaps needed
descriptor.autoGenerateMips = false;
// Create texture with descriptor
var texture = new RenderTexture(descriptor);
texture.name = "ProvinceID_RenderTexture";
texture.filterMode = FilterMode.Point; // No filtering for ID data
texture.wrapMode = TextureWrapMode.Clamp;
texture.Create();
return texture;
}
Format Selection Guide
For UAV-Enabled Textures (enableRandomWrite = true):
GraphicsFormat.R8G8B8A8_UNorm // ✅ ALWAYS use this - universal UAV support
⚠️ WARNING: R16G16_UNorm UAV Compatibility Issue
- Problem: R16G16_UNorm with
enableRandomWrite = truecreates TYPELESS format on some platforms - Symptom: UAV writes from compute shaders fail silently
- Solution: Always use R8G8B8A8_UNorm for UAV textures
- Discovered: 2025-10-26 - BorderTexture TYPELESS issue
- Reference: 4-smooth-borders-completion.md
For Province/Entity IDs (Integer Data):
GraphicsFormat.R8G8B8A8_UNorm // 4x 8-bit unsigned [0,255] → [0,1] - Best for UAV
GraphicsFormat.R16G16_UNorm // 2x 16-bit - ⚠️ AVOID for UAV (unreliable)
For Owner/Country IDs (Single Integer):
GraphicsFormat.R16_UNorm // 1x 16-bit unsigned (read-only)
GraphicsFormat.R32_SFloat // 1x 32-bit float (if needed)
For Color Data:
GraphicsFormat.R8G8B8A8_UNorm // Standard RGBA
GraphicsFormat.R8G8B8A8_SRGB // sRGB color space
Trade-offs
What We Gain
- ✅ Consistent format across platforms
- ✅ Clear error messages on unsupported formats
- ✅ Multiplayer-safe (deterministic GPU state)
- ✅ Future-proof (GraphicsFormat is the modern API)
What We Give Up
- ❌ None - explicit format is strictly better
- ❌ Slightly more verbose code (RenderTextureDescriptor vs constructor)
- But worth it for reliability
Documentation Impact
Updated:
- Map/FILE_REGISTRY.md - Noted R8G8B8A8_UNorm format for MapTextureManager
- learnings/unity-gpu-debugging-guide.md - Added TYPELESS gotcha section
Pattern Added:
- Always use RenderTextureDescriptor for UAV-enabled textures
- Verify format in RenderDoc when debugging
Verification
How to Verify Format is Correct
Method 1: RenderDoc
- Capture frame with F12
- Find your RenderTexture in Texture Viewer
- Check format in properties panel
- Should show:
DXGI_FORMAT_R8G8B8A8_UNORM(not TYPELESS)
Method 2: Code Logging
var texture = CreateRenderTexture();
Debug.Log($"Texture format: {texture.graphicsFormat}");
// Expected: R8G8B8A8_UNorm
// NOT: R8G8B8A8_TYPELESS or Unknown
Related Decisions
- fixed-point-determinism.md - CPU determinism via fixed-point
- This decision: GPU determinism via explicit formats
- Together: Full multiplayer determinism (CPU + GPU)
Gotchas for Future
Watch Out For:
-
enableRandomWrite AFTER descriptor creation
var tex = new RenderTexture(desc); tex.enableRandomWrite = true; // ❌ Too late! Recreates texture with TYPELESSFix: Set
descriptor.enableRandomWrite = trueBEFOREnew RenderTexture(descriptor) -
Assuming RenderTextureFormat is Enough
new RenderTexture(w, h, 0, RenderTextureFormat.ARGB32); // ❌ May become TYPELESSFix: Always use explicit GraphicsFormat
-
Not Verifying in RenderDoc
- Always check format in RenderDoc when debugging GPU issues
- TYPELESS looks fine in code, only visible in GPU debugger
Quick Reference
When to Use Explicit GraphicsFormat:
- ✅ Any RenderTexture with
enableRandomWrite = true - ✅ Any RenderTexture used as compute shader UAV
- ✅ Any RenderTexture where format matters for rendering
- ✅ All the time (it's never wrong to be explicit)
How to Use:
var descriptor = new RenderTextureDescriptor(
width, height,
UnityEngine.Experimental.Rendering.GraphicsFormat.R8G8B8A8_UNorm,
0
);
descriptor.enableRandomWrite = true;
var texture = new RenderTexture(descriptor);
Decision made 2025-10-05 after debugging ~1000 broken provinces caused by TYPELESS format