Skip to content

Commit ef29edb

Browse files
fix: OpenGL analysis tools and shader debug diagnostics
- list_passes: use enumeratePassRanges() instead of listPasses() so captures without debug markers return synthetic passes grouped by render target changes (fixes A1) - get_cbuffer_contents: resolve actual UBO binding via descriptor access system instead of passing ResourceId() — fixes all-zero values on OpenGL (fixes A2) - get_bindings: patch OpenGL bind points using GetDescriptorLocations() since fixedBindNumber in shader reflection is always 0 for GL (fixes A3) - debug_pixel/debug_vertex/debug_thread: distinguish "shader not debuggable" (DebugNotSupported with RenderDoc reason) from "no hit" to improve diagnostics when OpenGL shader debug fails Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e41bbb2 commit ef29edb

File tree

5 files changed

+202
-12
lines changed

5 files changed

+202
-12
lines changed

src/core/assertions.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include "core/errors.h"
33
#include "core/events.h"
44
#include "core/info.h"
5+
#include "core/pass_analysis.h"
56
#include "core/pipeline.h"
67
#include "core/pixel.h"
78
#include "core/resources.h"
@@ -184,7 +185,7 @@ AssertResult assertCount(const Session& session,
184185
auto buffers = ctrl->GetBuffers();
185186
actual = static_cast<int64_t>(buffers.size());
186187
} else if (what == "passes") {
187-
auto passes = listPasses(session);
188+
auto passes = enumeratePassRanges(session);
188189
actual = static_cast<int64_t>(passes.size());
189190
} else {
190191
throw CoreError(CoreError::Code::InternalError,

src/core/cbuffer.cpp

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -211,10 +211,6 @@ ShaderStageInfo getShaderStageInfo(IReplayController* ctrl, ShaderStage stage) {
211211
return info;
212212
}
213213

214-
// Note: We pass ResourceId() / 0 / 0 for the buffer descriptor and let
215-
// RenderDoc's GetCBufferVariableContents resolve the actual binding internally.
216-
// This works across all APIs and avoids complex per-API descriptor resolution.
217-
218214
} // anonymous namespace
219215

220216
std::vector<CBufferInfo> listCBuffers(const Session& session,
@@ -270,14 +266,42 @@ CBufferContents getCBufferContents(const Session& session,
270266

271267
const auto& cbMeta = refl.constantBlocks[cbufferIndex];
272268

273-
// Fetch variable contents — pass ResourceId() for buffer to let RenderDoc resolve
269+
// Resolve the actual buffer binding via the descriptor access system.
270+
// On OpenGL, UBO bindings are dynamic and cannot be inferred from shader
271+
// reflection alone — we query GetDescriptorAccess() to find the descriptor
272+
// that backs this constant block, then GetDescriptors() to get the actual
273+
// buffer ResourceId, offset, and size.
274274
::ShaderStage rdcStage = toRdcStage(stage);
275275
rdcstr entryPoint(stageInfo.entryPoint.c_str());
276276

277+
::ResourceId cbBufferId;
278+
uint64_t cbByteOffset = 0;
279+
uint64_t cbByteSize = 0;
280+
281+
const auto& accesses = ctrl->GetDescriptorAccess();
282+
for (int i = 0; i < accesses.count(); i++) {
283+
const auto& access = accesses[i];
284+
if (access.stage == rdcStage &&
285+
IsConstantBlockDescriptor(access.type) &&
286+
access.index == static_cast<uint16_t>(cbufferIndex) &&
287+
access.arrayElement == 0) {
288+
// Found the descriptor access — now fetch the descriptor contents.
289+
rdcarray<DescriptorRange> ranges;
290+
ranges.push_back(DescriptorRange(access));
291+
auto descriptors = ctrl->GetDescriptors(access.descriptorStore, ranges);
292+
if (!descriptors.empty()) {
293+
cbBufferId = descriptors[0].resource;
294+
cbByteOffset = descriptors[0].byteOffset;
295+
cbByteSize = descriptors[0].byteSize;
296+
}
297+
break;
298+
}
299+
}
300+
277301
rdcarray<::ShaderVariable> vars = ctrl->GetCBufferVariableContents(
278302
stageInfo.pipelineId, stageInfo.shaderId, rdcStage,
279303
entryPoint, cbufferIndex,
280-
::ResourceId(), 0, 0);
304+
cbBufferId, cbByteOffset, cbByteSize);
281305

282306
// Build result
283307
CBufferContents result;

src/core/debug.cpp

Lines changed: 107 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,79 @@ DebugLoopResult runDebugLoop(IReplayController* ctrl, ShaderDebugTrace* dbgTrace
172172
return result;
173173
}
174174

175+
// Check if a shader stage is marked as not debuggable by RenderDoc and return
176+
// the reason string. Returns empty string if the shader is debuggable or if
177+
// the check cannot be performed.
178+
std::string getShaderNotDebuggableReason(IReplayController* ctrl, ::ShaderStage stage) {
179+
APIProperties props = ctrl->GetAPIProperties();
180+
const ::ShaderReflection* refl = nullptr;
181+
182+
switch (props.pipelineType) {
183+
case GraphicsAPI::OpenGL: {
184+
const auto* s = ctrl->GetGLPipelineState();
185+
if (!s) break;
186+
switch (stage) {
187+
case ::ShaderStage::Vertex: refl = s->vertexShader.reflection; break;
188+
case ::ShaderStage::Pixel: refl = s->fragmentShader.reflection; break;
189+
case ::ShaderStage::Geometry: refl = s->geometryShader.reflection; break;
190+
case ::ShaderStage::Hull: refl = s->tessControlShader.reflection; break;
191+
case ::ShaderStage::Domain: refl = s->tessEvalShader.reflection; break;
192+
case ::ShaderStage::Compute: refl = s->computeShader.reflection; break;
193+
default: break;
194+
}
195+
break;
196+
}
197+
case GraphicsAPI::D3D11: {
198+
const auto* s = ctrl->GetD3D11PipelineState();
199+
if (!s) break;
200+
switch (stage) {
201+
case ::ShaderStage::Vertex: refl = s->vertexShader.reflection; break;
202+
case ::ShaderStage::Pixel: refl = s->pixelShader.reflection; break;
203+
case ::ShaderStage::Geometry: refl = s->geometryShader.reflection; break;
204+
case ::ShaderStage::Hull: refl = s->hullShader.reflection; break;
205+
case ::ShaderStage::Domain: refl = s->domainShader.reflection; break;
206+
case ::ShaderStage::Compute: refl = s->computeShader.reflection; break;
207+
default: break;
208+
}
209+
break;
210+
}
211+
case GraphicsAPI::D3D12: {
212+
const auto* s = ctrl->GetD3D12PipelineState();
213+
if (!s) break;
214+
switch (stage) {
215+
case ::ShaderStage::Vertex: refl = s->vertexShader.reflection; break;
216+
case ::ShaderStage::Pixel: refl = s->pixelShader.reflection; break;
217+
case ::ShaderStage::Geometry: refl = s->geometryShader.reflection; break;
218+
case ::ShaderStage::Hull: refl = s->hullShader.reflection; break;
219+
case ::ShaderStage::Domain: refl = s->domainShader.reflection; break;
220+
case ::ShaderStage::Compute: refl = s->computeShader.reflection; break;
221+
default: break;
222+
}
223+
break;
224+
}
225+
case GraphicsAPI::Vulkan: {
226+
const auto* s = ctrl->GetVulkanPipelineState();
227+
if (!s) break;
228+
switch (stage) {
229+
case ::ShaderStage::Vertex: refl = s->vertexShader.reflection; break;
230+
case ::ShaderStage::Pixel: refl = s->fragmentShader.reflection; break;
231+
case ::ShaderStage::Geometry: refl = s->geometryShader.reflection; break;
232+
case ::ShaderStage::Hull: refl = s->tessControlShader.reflection; break;
233+
case ::ShaderStage::Domain: refl = s->tessEvalShader.reflection; break;
234+
case ::ShaderStage::Compute: refl = s->computeShader.reflection; break;
235+
default: break;
236+
}
237+
break;
238+
}
239+
default: break;
240+
}
241+
242+
if (refl && !refl->debugInfo.debuggable)
243+
return std::string(refl->debugInfo.debugStatus.c_str());
244+
245+
return {};
246+
}
247+
175248
} // anonymous namespace
176249

177250
ShaderDebugResult debugPixel(
@@ -192,9 +265,22 @@ ShaderDebugResult debugPixel(
192265
ShaderDebugTrace* trace = ctrl->DebugPixel(x, y, inputs);
193266
if (!trace || !trace->debugger) {
194267
if (trace) ctrl->FreeTrace(trace);
268+
269+
// Distinguish "shader not debuggable" from "no fragment hit" so users
270+
// know whether the issue is a RenderDoc limitation or a wrong coordinate.
271+
std::string reason = getShaderNotDebuggableReason(ctrl, ::ShaderStage::Pixel);
272+
if (!reason.empty())
273+
throw CoreError(CoreError::Code::DebugNotSupported,
274+
"Fragment shader is not debuggable at event " +
275+
std::to_string(eventId) + ": " + reason);
276+
195277
throw CoreError(CoreError::Code::NoFragmentFound,
196-
"No debuggable fragment at (" + std::to_string(x) +
197-
"," + std::to_string(y) + ") for event " + std::to_string(eventId));
278+
"No fragment hit at (" + std::to_string(x) +
279+
"," + std::to_string(y) + ") for event " + std::to_string(eventId) +
280+
". The shader is marked as debuggable, but RenderDoc could "
281+
"not produce a debug trace. This can happen with certain "
282+
"OpenGL shader features that are not fully supported by "
283+
"the software shader debugger.");
198284
}
199285

200286
ShaderDebugResult result;
@@ -233,9 +319,20 @@ ShaderDebugResult debugVertex(
233319
ShaderDebugTrace* trace = ctrl->DebugVertex(vertexId, instance, idx, view);
234320
if (!trace || !trace->debugger) {
235321
if (trace) ctrl->FreeTrace(trace);
322+
323+
std::string reason = getShaderNotDebuggableReason(ctrl, ::ShaderStage::Vertex);
324+
if (!reason.empty())
325+
throw CoreError(CoreError::Code::DebugNotSupported,
326+
"Vertex shader is not debuggable at event " +
327+
std::to_string(eventId) + ": " + reason);
328+
236329
throw CoreError(CoreError::Code::NoFragmentFound,
237330
"Cannot debug vertex " + std::to_string(vertexId) +
238-
" at event " + std::to_string(eventId));
331+
" at event " + std::to_string(eventId) +
332+
". The shader is marked as debuggable, but RenderDoc could "
333+
"not produce a debug trace. This can happen with certain "
334+
"OpenGL shader features that are not fully supported by "
335+
"the software shader debugger.");
239336
}
240337

241338
ShaderDebugResult result;
@@ -279,6 +376,13 @@ ShaderDebugResult debugThread(
279376
ShaderDebugTrace* trace = ctrl->DebugThread(groupid, threadid);
280377
if (!trace || !trace->debugger) {
281378
if (trace) ctrl->FreeTrace(trace);
379+
380+
std::string reason = getShaderNotDebuggableReason(ctrl, ::ShaderStage::Compute);
381+
if (!reason.empty())
382+
throw CoreError(CoreError::Code::DebugNotSupported,
383+
"Compute shader is not debuggable at event " +
384+
std::to_string(eventId) + ": " + reason);
385+
282386
throw CoreError(CoreError::Code::NoFragmentFound,
283387
"Cannot debug thread (" + std::to_string(threadX) + "," +
284388
std::to_string(threadY) + "," + std::to_string(threadZ) +

src/core/pipeline.cpp

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,64 @@ StageBindings extractStageBindings(const ::ShaderReflection* refl, ::ResourceId
5353
return bindings;
5454
}
5555

56+
// On OpenGL, fixedBindNumber in shader reflection is always 0 (bindings are
57+
// dynamic). Resolve actual bind points via the descriptor system.
58+
void patchGLBindPoints(IReplayController* ctrl, const GLPipe::State* glState,
59+
std::map<ShaderStage, StageBindings>& result) {
60+
if (!glState) return;
61+
62+
const auto& accesses = ctrl->GetDescriptorAccess();
63+
if (accesses.empty()) return;
64+
65+
// Build DescriptorRanges from accesses and query logical locations in one batch.
66+
rdcarray<DescriptorRange> ranges;
67+
ranges.reserve(accesses.size());
68+
for (int i = 0; i < accesses.count(); i++)
69+
ranges.push_back(DescriptorRange(accesses[i]));
70+
71+
auto locations = ctrl->GetDescriptorLocations(glState->descriptorStore, ranges);
72+
73+
// Map: (stage, descriptorType category, reflection index) -> fixedBindNumber
74+
for (int i = 0; i < accesses.count() && i < locations.count(); i++) {
75+
const auto& access = accesses[i];
76+
const auto& loc = locations[i];
77+
78+
// Map RenderDoc ShaderStage to our ShaderStage enum
79+
ShaderStage stage;
80+
switch (access.stage) {
81+
case ::ShaderStage::Vertex: stage = ShaderStage::Vertex; break;
82+
case ::ShaderStage::Hull: stage = ShaderStage::Hull; break;
83+
case ::ShaderStage::Domain: stage = ShaderStage::Domain; break;
84+
case ::ShaderStage::Geometry: stage = ShaderStage::Geometry; break;
85+
case ::ShaderStage::Pixel: stage = ShaderStage::Pixel; break;
86+
case ::ShaderStage::Compute: stage = ShaderStage::Compute; break;
87+
default: continue;
88+
}
89+
90+
auto it = result.find(stage);
91+
if (it == result.end()) continue;
92+
93+
auto& bindings = it->second;
94+
uint16_t idx = access.index;
95+
uint32_t bindNum = loc.fixedBindNumber;
96+
97+
auto cat = CategoryForDescriptorType(access.type);
98+
if (cat == DescriptorCategory::ConstantBlock) {
99+
if (idx < bindings.constantBuffers.size())
100+
bindings.constantBuffers[idx].bindPoint = bindNum;
101+
} else if (cat == DescriptorCategory::ReadOnlyResource) {
102+
if (idx < bindings.readOnlyResources.size())
103+
bindings.readOnlyResources[idx].bindPoint = bindNum;
104+
} else if (cat == DescriptorCategory::ReadWriteResource) {
105+
if (idx < bindings.readWriteResources.size())
106+
bindings.readWriteResources[idx].bindPoint = bindNum;
107+
} else if (cat == DescriptorCategory::Sampler) {
108+
if (idx < bindings.samplers.size())
109+
bindings.samplers[idx].bindPoint = bindNum;
110+
}
111+
}
112+
}
113+
56114
} // anonymous namespace
57115

58116
PipelineState getPipelineState(const Session& session,
@@ -546,6 +604,8 @@ std::map<ShaderStage, StageBindings> getBindings(const Session& session,
546604
result[ShaderStage::Geometry] = extractStageBindings(state->geometryShader.reflection, state->geometryShader.shaderResourceId);
547605
if (state->computeShader.reflection)
548606
result[ShaderStage::Compute] = extractStageBindings(state->computeShader.reflection, state->computeShader.shaderResourceId);
607+
// Patch bind points with actual GL descriptor locations
608+
patchGLBindPoints(ctrl, state, result);
549609
break;
550610
}
551611
case GraphicsAPI::Vulkan: {

src/mcp/tools/resource_tools.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include "mcp/tool_registry.h"
33
#include "mcp/serialization.h"
44
#include "core/session.h"
5+
#include "core/pass_analysis.h"
56
#include "core/resources.h"
67

78
namespace renderdoc::mcp::tools {
@@ -49,12 +50,12 @@ void registerResourceTools(ToolRegistry& registry) {
4950
// list_passes
5051
registry.registerTool({
5152
"list_passes",
52-
"List all render passes in the capture (marker regions containing draw or dispatch calls)",
53+
"List all render passes in the capture. Returns marker-based passes when available, otherwise synthetic passes grouped by render target changes.",
5354
{{"type", "object"},
5455
{"properties", nlohmann::json::object()}},
5556
[](mcp::ToolContext& ctx, const nlohmann::json& /*args*/) -> nlohmann::json {
5657
auto& session = ctx.session;
57-
auto passes = core::listPasses(session);
58+
auto passes = core::enumeratePassRanges(session);
5859
nlohmann::json result;
5960
result["passes"] = to_json_array(passes);
6061
result["count"] = passes.size();

0 commit comments

Comments
 (0)