using System; using System.Collections.Generic; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; using UnityEngine.Assertions; namespace UnityEngine.Rendering { /// /// Internal development tool. /// Manages gpu-buffers for shader debug printing. /// public sealed class ShaderDebugPrintManager { private static readonly ShaderDebugPrintManager s_Instance = new ShaderDebugPrintManager(); private const int k_FramesInFlight = 4; private const int k_MaxBufferElements = 1024 * 16; // Must match the shader size definition private List m_OutputBuffers = new List(); private List m_ReadbackRequests = new List(); // Cache Action to avoid delegate allocation private Action m_BufferReadCompleteAction; private int m_FrameCounter = 0; private bool m_FrameCleared = false; private string m_OutputLine = ""; private Action m_OutputAction; private static readonly int m_ShaderPropertyIDInputMouse = Shader.PropertyToID("_ShaderDebugPrintInputMouse"); private static readonly int m_ShaderPropertyIDInputFrame = Shader.PropertyToID("_ShaderDebugPrintInputFrame"); private static readonly int m_shaderDebugOutputData = Shader.PropertyToID("shaderDebugOutputData"); // A static "container" for all profiler markers. private static class Profiling { // Uses nameof to avoid aliasing public static readonly ProfilingSampler BufferReadComplete = new ProfilingSampler($"{nameof(ShaderDebugPrintManager)}.{nameof(BufferReadComplete)}"); } // Should match: com.unity.render-pipelines.core/ShaderLibrary/ShaderDebugPrint.hlsl enum DebugValueType { TypeUint = 1, TypeInt = 2, TypeFloat = 3, TypeUint2 = 4, TypeInt2 = 5, TypeFloat2 = 6, TypeUint3 = 7, TypeInt3 = 8, TypeFloat3 = 9, TypeUint4 = 10, TypeInt4 = 11, TypeFloat4 = 12, TypeBool = 13, }; private const uint k_TypeHasTag = 128; private int DebugValueTypeToElemSize(DebugValueType type) { switch (type) { case DebugValueType.TypeUint: case DebugValueType.TypeInt: case DebugValueType.TypeFloat: case DebugValueType.TypeBool: return 1; case DebugValueType.TypeUint2: case DebugValueType.TypeInt2: case DebugValueType.TypeFloat2: return 2; case DebugValueType.TypeUint3: case DebugValueType.TypeInt3: case DebugValueType.TypeFloat3: return 3; case DebugValueType.TypeUint4: case DebugValueType.TypeInt4: case DebugValueType.TypeFloat4: return 4; default: return 0; } } private ShaderDebugPrintManager() { for (int i = 0; i < k_FramesInFlight; i++) { m_OutputBuffers.Add(new GraphicsBuffer(GraphicsBuffer.Target.Structured, k_MaxBufferElements, 4)); m_ReadbackRequests.Add(new Rendering.AsyncGPUReadbackRequest()); } m_BufferReadCompleteAction = BufferReadComplete; m_OutputAction = DefaultOutput; } /// /// Get the current instance. /// public static ShaderDebugPrintManager instance => s_Instance; /// /// Set shader input constants. /// /// CommandBuffer to store the commands. /// Input parameters for the constants. public void SetShaderDebugPrintInputConstants(CommandBuffer cmd, ShaderDebugPrintInput input) { var mouse = new Vector4(input.pos.x, input.pos.y, input.leftDown ? 1 : 0, input.rightDown ? 1 : 0); cmd.SetGlobalVector(m_ShaderPropertyIDInputMouse, mouse); cmd.SetGlobalInt(m_ShaderPropertyIDInputFrame, m_FrameCounter); } /// /// Binds the gpu-buffers for current frame. /// /// CommandBuffer to store the commands. public void SetShaderDebugPrintBindings(CommandBuffer cmd) { int index = m_FrameCounter % k_FramesInFlight; if (!m_ReadbackRequests[index].done) { // We shouldn't end up here too often m_ReadbackRequests[index].WaitForCompletion(); } cmd.SetGlobalBuffer(m_shaderDebugOutputData, m_OutputBuffers[index]); ClearShaderDebugPrintBuffer(); } private void ClearShaderDebugPrintBuffer() { // Only clear the buffer the first time this is called in each frame if (!m_FrameCleared) { int index = m_FrameCounter % k_FramesInFlight; NativeArray data = new NativeArray(1, Allocator.Temp); data[0] = 0; m_OutputBuffers[index].SetData(data, 0, 0, 1); m_FrameCleared = true; } } private void BufferReadComplete(Rendering.AsyncGPUReadbackRequest request) { using var profScope = new ProfilingScope(Profiling.BufferReadComplete); Assert.IsTrue(request.done); if (!request.hasError) { NativeArray data = request.GetData(0); uint count = data[0]; if (count >= k_MaxBufferElements) { count = k_MaxBufferElements; // Shader print buffer is full, some data is lost! Debug.LogWarning("Debug Shader Print Buffer Full!"); } string newOutputLine = ""; if (count > 0) newOutputLine += "Frame #" + m_FrameCounter + ": "; unsafe // Need to do ugly casts via pointers { uint* ptr = (uint*)data.GetUnsafePtr(); for (int i = 1; i < count;) { DebugValueType type = (DebugValueType)(data[i] & 0x0f); bool hasTag = (data[i] & k_TypeHasTag) == k_TypeHasTag; // ensure elem for tag after the header if (hasTag && i + 1 < count) { uint tagEncoded = data[i + 1]; i++; for (int j = 0; j < 4; j++) { char c = (char)(tagEncoded & 255); // skip '\0', for low-level output (avoid string termination) if (c == 0) continue; newOutputLine += c; tagEncoded >>= 8; } newOutputLine += " "; } // ensure elem for payload after the header/tag int elemSize = DebugValueTypeToElemSize(type); if (i + elemSize > count) break; i++; // [i] == payload switch (type) { case DebugValueType.TypeUint: { newOutputLine += $"{data[i]}u"; break; } case DebugValueType.TypeInt: { int valueInt = *(int*)&ptr[i]; newOutputLine += valueInt; break; } case DebugValueType.TypeFloat: { float valueFloat = *(float*)&ptr[i]; newOutputLine += $"{valueFloat}f"; break; } case DebugValueType.TypeUint2: { uint* valueUint2 = &ptr[i]; newOutputLine += $"uint2({valueUint2[0]}, {valueUint2[1]})"; break; } case DebugValueType.TypeInt2: { int* valueInt2 = (int*)&ptr[i]; newOutputLine += $"int2({valueInt2[0]}, {valueInt2[1]})"; break; } case DebugValueType.TypeFloat2: { float* valueFloat2 = (float*)&ptr[i]; newOutputLine += $"float2({valueFloat2[0]}, {valueFloat2[1]})"; break; } case DebugValueType.TypeUint3: { uint* valueUint3 = &ptr[i]; newOutputLine += $"uint3({valueUint3[0]}, {valueUint3[1]}, {valueUint3[2]})"; break; } case DebugValueType.TypeInt3: { int* valueInt3 = (int*)&ptr[i]; newOutputLine += $"int3({valueInt3[0]}, {valueInt3[1]}, {valueInt3[2]})"; break; } case DebugValueType.TypeFloat3: { float* valueFloat3 = (float*)&ptr[i]; newOutputLine += $"float3({valueFloat3[0]}, {valueFloat3[1]}, {valueFloat3[2]})"; break; } case DebugValueType.TypeUint4: { uint* valueUint4 = &ptr[i]; newOutputLine += $"uint4({valueUint4[0]}, {valueUint4[1]}, {valueUint4[2]}, {valueUint4[3]})"; break; } case DebugValueType.TypeInt4: { int* valueInt4 = (int*)&ptr[i]; newOutputLine += $"int4({valueInt4[0]}, {valueInt4[1]}, {valueInt4[2]}, {valueInt4[3]})"; break; } case DebugValueType.TypeFloat4: { float* valueFloat4 = (float*)&ptr[i]; newOutputLine += $"float4({valueFloat4[0]}, {valueFloat4[1]}, {valueFloat4[2]}, {valueFloat4[3]})"; break; } case DebugValueType.TypeBool: { newOutputLine += ((data[i] == 0) ? "False" : "True"); break; } default: i = (int)count; // Cannot handle the rest if there is an unknown type break; } i += elemSize; newOutputLine += " "; } } if (count > 0) { m_OutputLine = newOutputLine; m_OutputAction(newOutputLine); } } else { const string errorMsg = "Error at read back!"; m_OutputLine = errorMsg; m_OutputAction(errorMsg); } } /// /// Initiate async read-back of the GPU buffer to the CPU. /// Prepare a new GPU buffer for the next frame. /// public void EndFrame() { int index = m_FrameCounter % k_FramesInFlight; m_ReadbackRequests[index] = Rendering.AsyncGPUReadback.Request(m_OutputBuffers[index], m_BufferReadCompleteAction); m_FrameCounter++; m_FrameCleared = false; } /// /// Initiate synchronous read-back of the GPU buffer to the CPU and executes output action. By default prints to the debug log. /// public void PrintImmediate() { int index = m_FrameCounter % k_FramesInFlight; var request = Rendering.AsyncGPUReadback.Request(m_OutputBuffers[index]); request.WaitForCompletion(); m_BufferReadCompleteAction(request); m_FrameCounter++; m_FrameCleared = false; } // Custom output API /// /// Get current print line. /// public string outputLine { get => m_OutputLine; } /// /// Action taken for each print line. By default prints to the debug log. /// public Action outputAction { set => m_OutputAction = value; } /// /// The default output action. Print to the debug log. /// /// Line to be printed. public void DefaultOutput(string line) { Debug.Log(line); } } /// /// Shader constant input parameters. /// public struct ShaderDebugPrintInput { // Mouse input for the shader /// /// Mouse position. /// GameView bottom-left == (0,0) top-right == (surface.width, surface.height) where surface == game display surface/rendertarget /// For screen pixel coordinates, game-view should be set to "Free Aspect". /// Works only in PlayMode. /// public Vector2 pos { get; set; } /// /// Left mouse button is pressed. /// public bool leftDown { get; set; } /// /// Right mouse button is pressed. /// public bool rightDown { get; set; } /// /// Middle mouse button is pressed. /// public bool middleDown { get; set; } /// /// Pretty print parameters for debug purposes. /// /// A string containing debug information // NOTE: Separate from ToString on purpose. public string String() { return $"Mouse: {pos.x}x{pos.y} Btns: Left:{leftDown} Right:{rightDown} Middle:{middleDown} "; } } /// /// Reads system input to produce ShaderDebugPrintInput parameters. /// public static class ShaderDebugPrintInputProducer { /// /// Read system input. /// /// Input parameters for ShaderDebugPrintManager. static public ShaderDebugPrintInput Get() { var r = new ShaderDebugPrintInput(); #if ENABLE_LEGACY_INPUT_MANAGER r.pos = Input.mousePosition; r.leftDown = Input.GetMouseButton(0); r.rightDown = Input.GetMouseButton(1); r.middleDown = Input.GetMouseButton(2); #endif #if ENABLE_INPUT_SYSTEM && ENABLE_INPUT_SYSTEM_PACKAGE // NOTE: needs Unity.InputSystem asmdef reference. var mouse = InputSystem.Mouse.current; r.pos = mouse.position.ReadValue(); r.leftDown = mouse.leftButton.isPressed; r.rightDown = mouse.rightButton.isPressed; r.middleDown = mouse.middleButton.isPressed; #endif return r; } } }