Validate OpenGL texture binding slots

This commit is contained in:
2026-06-02 21:14:07 +02:00
parent d664e9fc39
commit 55b725e876
7 changed files with 99 additions and 17 deletions

View File

@@ -275,13 +275,16 @@ Known local toolchain state:
blit filters into GL-facing planned command data while rejecting unsupported
enum tokens before a real GL context is needed. It also plans whole recorded
command streams, preserving per-command planned data while counting render
passes, draws, shader binds, shader uniforms, texture uploads, mipmap
generation, texture transitions, texture copies, texture readbacks, frame
captures, passthrough commands, trace commands, unsupported commands, and
render-pass ordering errors such as state changes outside a pass, nested
passes, and unclosed passes. It also validates executable command
dependencies, including shader-before-uniform and shader-plus-mesh before
draw within each render pass.
passes, draws, shader binds, shader uniforms, texture/sampler binds, texture
uploads, mipmap generation, texture transitions, texture copies, texture
readbacks, frame captures, passthrough commands, trace commands, unsupported
commands, and render-pass ordering errors such as state changes outside a
pass, nested passes, and unclosed passes. It also validates executable
command dependencies, including shader-before-uniform and shader-plus-mesh
before draw within each render pass, and rejects invalid texture/sampler bind
slots in malformed recorded streams. `pano_cli record-render` emits the
OpenGL plan texture/sampler bind counts so automation can assert backend
interpretation without an OpenGL context.
Desktop VR drawing also consumes backend-owned scissor/depth/blend state,
depth clear masks, active texture units, and fallback 2D texture unbind
targets while retaining the existing VR SDK/platform bridge shape.

View File

@@ -534,13 +534,14 @@ blend/depth/sampler state, texture formats, primitive modes, draw counts, and
blit filters into GL-facing planned command data with explicit unsupported-token
rejection before a runtime GL context is needed. It also plans whole recorded
command streams, preserving per-command planned data while counting render
passes, draws, shader binds, shader uniforms, texture uploads, mipmap
generation, texture transitions, texture copies, texture readbacks, frame
captures, passthrough commands, trace commands, unsupported commands, and
render-pass ordering errors such as state changes outside a pass, nested
passes, and unclosed passes. It also validates executable command dependencies,
including shader-before-uniform and shader-plus-mesh before draw within each
render pass.
passes, draws, shader binds, shader uniforms, texture/sampler binds, texture
uploads, mipmap generation, texture transitions, texture copies, texture
readbacks, frame captures, passthrough commands, trace commands, unsupported
commands, and render-pass ordering errors such as state changes outside a pass,
nested passes, and unclosed passes. It also validates executable command
dependencies, including shader-before-uniform and shader-plus-mesh before draw
within each render pass, and rejects invalid texture/sampler bind slots in
malformed recorded streams.
The existing renderer classes are not yet fully
behind the renderer interfaces.

View File

@@ -191,12 +191,16 @@ OpenGlPlannedCommand plan_recorded_render_command(pp::renderer::RecordedRenderCo
case pp::renderer::RecordedRenderCommandKind::bind_texture:
planned.kind = OpenGlPlannedCommandKind::bind_texture;
planned.texture_format = texture_format_for_renderer_format(command.texture_desc.format);
planned.supported = texture_format_supported(planned.texture_format);
planned.texture_slot = command.texture_slot;
planned.supported = texture_format_supported(planned.texture_format)
&& planned.texture_slot < pp::renderer::max_texture_slots;
break;
case pp::renderer::RecordedRenderCommandKind::bind_sampler:
planned.kind = OpenGlPlannedCommandKind::bind_sampler;
planned.sampler = sampler_state_for_renderer_sampler_desc(command.sampler_desc);
planned.supported = planned.sampler.supported;
planned.sampler_slot = command.sampler_slot;
planned.supported = planned.sampler.supported
&& planned.sampler_slot < pp::renderer::max_texture_slots;
break;
case pp::renderer::RecordedRenderCommandKind::bind_mesh:
planned.kind = OpenGlPlannedCommandKind::bind_mesh;
@@ -346,6 +350,18 @@ OpenGlCommandPlan plan_recorded_render_commands(
record_dependency_error(plan, index);
}
break;
case OpenGlPlannedCommandKind::bind_texture:
++plan.texture_bind_command_count;
if (!in_render_pass) {
record_render_pass_order_error(plan, index);
}
break;
case OpenGlPlannedCommandKind::bind_sampler:
++plan.sampler_bind_command_count;
if (!in_render_pass) {
record_render_pass_order_error(plan, index);
}
break;
case OpenGlPlannedCommandKind::bind_mesh:
if (!in_render_pass) {
record_render_pass_order_error(plan, index);

View File

@@ -53,6 +53,8 @@ struct OpenGlPlannedCommand {
pp::renderer::ReadbackRegion readback_region;
pp::renderer::ReadbackRegion source_region;
pp::renderer::ReadbackRegion destination_region;
std::uint32_t texture_slot = 0;
std::uint32_t sampler_slot = 0;
std::uint64_t upload_bytes = 0;
std::uint32_t generated_mip_levels = 0;
std::uint64_t generated_mip_bytes = 0;
@@ -79,6 +81,8 @@ struct OpenGlCommandPlan {
std::uint32_t draw_command_count = 0;
std::uint32_t shader_bind_command_count = 0;
std::uint32_t uniform_command_count = 0;
std::uint32_t texture_bind_command_count = 0;
std::uint32_t sampler_bind_command_count = 0;
std::uint32_t upload_command_count = 0;
std::uint32_t mipmap_command_count = 0;
std::uint32_t transition_command_count = 0;

View File

@@ -384,7 +384,7 @@ if(TARGET pano_cli)
COMMAND pano_cli record-render --width 32 --height 16)
set_tests_properties(pano_cli_record_render_smoke PROPERTIES
LABELS "renderer;integration;desktop-fast"
PASS_REGULAR_EXPRESSION "\"backend\":\"recording\".*\"framebufferFetch\":false.*\"explicitTextureTransitions\":true.*\"textureCopy\":true.*\"renderTargetBlit\":true.*\"frameCapture\":true.*\"float16RenderTargets\":false.*\"float32RenderTargets\":false.*\"width\":32.*\"height\":16.*\"createdResources\":7.*\"exercisedClearReset\":false.*\"clearRejectedOrphanedTraceEnd\":false.*\"clearReusedRenderPass\":false.*\"labeledCommandDescriptors\":16.*\"commands\":25.*\"openGlPlan\":.*\"available\":true.*\"supported\":true.*\"commands\":25.*\"renderPasses\":1.*\"drawCommands\":1.*\"shaderBindCommands\":1.*\"uniformCommands\":1.*\"uploadCommands\":1.*\"mipmapCommands\":1.*\"transitionCommands\":4.*\"copyCommands\":1.*\"readbackCommands\":1.*\"captureCommands\":1.*\"passthroughCommands\":0.*\"traceCommands\":3.*\"unsupportedCommands\":0.*\"renderPassOrderErrors\":0.*\"dependencyErrors\":0.*\"endedInRenderPass\":false.*\"renderPasses\":1.*\"depthClears\":1.*\"stencilClears\":0.*\"drawCommands\":1.*\"drawVertices\":3.*\"drawIndices\":3.*\"scissorCommands\":1.*\"blendCommands\":1.*\"depthCommands\":1.*\"uniformCommands\":1.*\"uniformBytes\":64.*\"bindTextureCommands\":1.*\"bindSamplerCommands\":1.*\"boundTextureBytes\":2048.*\"uploadCommands\":1.*\"uploadBytes\":4.*\"mipmapCommands\":1.*\"mipmapLevels\":3.*\"mipmapBytes\":84.*\"transitionCommands\":4.*\"transitionToUpload\":1.*\"transitionToShaderRead\":2.*\"copyCommands\":1.*\"copySourceBytes\":2048.*\"copyDestinationBytes\":2048.*\"readbackCommands\":1.*\"readbackBytes\":2048.*\"captureCommands\":1.*\"captureBytes\":2048.*\"blitCommands\":1.*\"blitSourceBytes\":2048.*\"blitDestinationBytes\":2048.*\"traceMarkers\":1.*\"traceBeginScopes\":1.*\"traceEndScopes\":1")
PASS_REGULAR_EXPRESSION "\"backend\":\"recording\".*\"framebufferFetch\":false.*\"explicitTextureTransitions\":true.*\"textureCopy\":true.*\"renderTargetBlit\":true.*\"frameCapture\":true.*\"float16RenderTargets\":false.*\"float32RenderTargets\":false.*\"width\":32.*\"height\":16.*\"createdResources\":7.*\"exercisedClearReset\":false.*\"clearRejectedOrphanedTraceEnd\":false.*\"clearReusedRenderPass\":false.*\"labeledCommandDescriptors\":16.*\"commands\":25.*\"openGlPlan\":.*\"available\":true.*\"supported\":true.*\"commands\":25.*\"renderPasses\":1.*\"drawCommands\":1.*\"shaderBindCommands\":1.*\"uniformCommands\":1.*\"textureBindCommands\":1.*\"samplerBindCommands\":1.*\"uploadCommands\":1.*\"mipmapCommands\":1.*\"transitionCommands\":4.*\"copyCommands\":1.*\"readbackCommands\":1.*\"captureCommands\":1.*\"passthroughCommands\":0.*\"traceCommands\":3.*\"unsupportedCommands\":0.*\"renderPassOrderErrors\":0.*\"dependencyErrors\":0.*\"endedInRenderPass\":false.*\"renderPasses\":1.*\"depthClears\":1.*\"stencilClears\":0.*\"drawCommands\":1.*\"drawVertices\":3.*\"drawIndices\":3.*\"scissorCommands\":1.*\"blendCommands\":1.*\"depthCommands\":1.*\"uniformCommands\":1.*\"uniformBytes\":64.*\"bindTextureCommands\":1.*\"bindSamplerCommands\":1.*\"boundTextureBytes\":2048.*\"uploadCommands\":1.*\"uploadBytes\":4.*\"mipmapCommands\":1.*\"mipmapLevels\":3.*\"mipmapBytes\":84.*\"transitionCommands\":4.*\"transitionToUpload\":1.*\"transitionToShaderRead\":2.*\"copyCommands\":1.*\"copySourceBytes\":2048.*\"copyDestinationBytes\":2048.*\"readbackCommands\":1.*\"readbackBytes\":2048.*\"captureCommands\":1.*\"captureBytes\":2048.*\"blitCommands\":1.*\"blitSourceBytes\":2048.*\"blitDestinationBytes\":2048.*\"traceMarkers\":1.*\"traceBeginScopes\":1.*\"traceEndScopes\":1")
add_test(NAME pano_cli_record_render_exercises_clear_reset
COMMAND pano_cli record-render --width 32 --height 16 --exercise-clear)

View File

@@ -164,6 +164,7 @@ void maps_binding_draw_and_blit_commands(pp::tests::Harness& h)
.extent = pp::renderer::Extent2D { .width = 16U, .height = 16U },
.format = pp::renderer::TextureFormat::depth24_stencil8,
},
.texture_slot = 3U,
});
const auto sampler = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand {
.kind = pp::renderer::RecordedRenderCommandKind::bind_sampler,
@@ -175,6 +176,7 @@ void maps_binding_draw_and_blit_commands(pp::tests::Harness& h)
.address_v = pp::renderer::SamplerAddressMode::mirrored_repeat,
.address_w = pp::renderer::SamplerAddressMode::clamp_to_border,
},
.sampler_slot = 3U,
});
const auto mesh = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand {
.kind = pp::renderer::RecordedRenderCommandKind::bind_mesh,
@@ -191,9 +193,11 @@ void maps_binding_draw_and_blit_commands(pp::tests::Harness& h)
PP_EXPECT(h, texture.supported);
PP_EXPECT(h, texture.requires_render_pass);
PP_EXPECT(h, texture.texture_format.internal_format == 0x88F0U);
PP_EXPECT(h, texture.texture_slot == 3U);
PP_EXPECT(h, sampler.supported);
PP_EXPECT(h, sampler.sampler.min_filter == 0x2702U);
PP_EXPECT(h, sampler.sampler.wrap_t == 0x8370U);
PP_EXPECT(h, sampler.sampler_slot == 3U);
PP_EXPECT(h, mesh.supported);
PP_EXPECT(h, mesh.primitive_mode == 0x0005U);
PP_EXPECT(h, draw.supported);
@@ -321,6 +325,18 @@ void rejects_unsupported_command_tokens(pp::tests::Harness& h)
.address_u = static_cast<pp::renderer::SamplerAddressMode>(255U),
},
});
const auto bad_texture_slot = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand {
.kind = pp::renderer::RecordedRenderCommandKind::bind_texture,
.texture_desc = pp::renderer::TextureDesc {
.extent = pp::renderer::Extent2D { .width = 1U, .height = 1U },
.format = pp::renderer::TextureFormat::rgba8,
},
.texture_slot = pp::renderer::max_texture_slots,
});
const auto bad_sampler_slot = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand {
.kind = pp::renderer::RecordedRenderCommandKind::bind_sampler,
.sampler_slot = pp::renderer::max_texture_slots,
});
const auto bad_mesh = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand {
.kind = pp::renderer::RecordedRenderCommandKind::draw,
.mesh_desc = pp::renderer::MeshDesc {
@@ -349,6 +365,10 @@ void rejects_unsupported_command_tokens(pp::tests::Harness& h)
PP_EXPECT(h, bad_depth.depth.compare_function == 0U);
PP_EXPECT(h, !bad_sampler.supported);
PP_EXPECT(h, bad_sampler.sampler.wrap_s == 0U);
PP_EXPECT(h, !bad_texture_slot.supported);
PP_EXPECT(h, bad_texture_slot.texture_slot == pp::renderer::max_texture_slots);
PP_EXPECT(h, !bad_sampler_slot.supported);
PP_EXPECT(h, bad_sampler_slot.sampler_slot == pp::renderer::max_texture_slots);
PP_EXPECT(h, !bad_mesh.supported);
PP_EXPECT(h, bad_mesh.primitive_mode == 0U);
PP_EXPECT(h, !bad_blit.supported);
@@ -404,6 +424,8 @@ void plans_valid_recorded_command_streams(pp::tests::Harness& h)
PP_EXPECT(h, plan.draw_command_count == 1U);
PP_EXPECT(h, plan.shader_bind_command_count == 1U);
PP_EXPECT(h, plan.uniform_command_count == 1U);
PP_EXPECT(h, plan.texture_bind_command_count == 0U);
PP_EXPECT(h, plan.sampler_bind_command_count == 0U);
PP_EXPECT(h, plan.passthrough_command_count == 0U);
PP_EXPECT(h, plan.trace_command_count == 1U);
PP_EXPECT(h, plan.unsupported_command_count == 0U);
@@ -421,6 +443,39 @@ void plans_valid_recorded_command_streams(pp::tests::Harness& h)
PP_EXPECT(h, plan.commands[8].kind == pp::renderer::gl::OpenGlPlannedCommandKind::blit_render_target);
}
void counts_texture_and_sampler_bindings_in_streams(pp::tests::Harness& h)
{
pp::renderer::RecordedRenderCommand texture_command;
texture_command.kind = pp::renderer::RecordedRenderCommandKind::bind_texture;
texture_command.texture_desc.format = pp::renderer::TextureFormat::rgba8;
texture_command.texture_slot = 2U;
pp::renderer::RecordedRenderCommand sampler_command;
sampler_command.kind = pp::renderer::RecordedRenderCommandKind::bind_sampler;
sampler_command.sampler_slot = 2U;
const std::vector<pp::renderer::RecordedRenderCommand> commands {
begin_render_pass_command(),
bind_shader_command("shader"),
texture_command,
sampler_command,
bind_mesh_command(),
draw_command(),
command_with_kind(pp::renderer::RecordedRenderCommandKind::end_render_pass),
};
const auto plan = pp::renderer::gl::plan_recorded_render_commands(commands);
PP_EXPECT(h, plan.supported);
PP_EXPECT(h, plan.texture_bind_command_count == 1U);
PP_EXPECT(h, plan.sampler_bind_command_count == 1U);
PP_EXPECT(h, plan.unsupported_command_count == 0U);
PP_EXPECT(h, plan.render_pass_order_error_count == 0U);
PP_EXPECT(h, plan.dependency_error_count == 0U);
PP_EXPECT(h, plan.commands[2].texture_slot == 2U);
PP_EXPECT(h, plan.commands[3].sampler_slot == 2U);
}
void flags_broken_render_pass_command_order(pp::tests::Harness& h)
{
const std::vector<pp::renderer::RecordedRenderCommand> outside_pass {
@@ -594,6 +649,7 @@ int main()
harness.run("rejects_unsupported_command_tokens", rejects_unsupported_command_tokens);
harness.run("names_planned_command_kinds", names_planned_command_kinds);
harness.run("plans_valid_recorded_command_streams", plans_valid_recorded_command_streams);
harness.run("counts_texture_and_sampler_bindings_in_streams", counts_texture_and_sampler_bindings_in_streams);
harness.run("flags_broken_render_pass_command_order", flags_broken_render_pass_command_order);
harness.run("flags_missing_render_dependencies", flags_missing_render_dependencies);
harness.run("tracks_unsupported_commands_in_streams", tracks_unsupported_commands_in_streams);

View File

@@ -2787,6 +2787,8 @@ int record_render(int argc, char** argv)
<< ",\"drawCommands\":" << open_gl_plan.draw_command_count
<< ",\"shaderBindCommands\":" << open_gl_plan.shader_bind_command_count
<< ",\"uniformCommands\":" << open_gl_plan.uniform_command_count
<< ",\"textureBindCommands\":" << open_gl_plan.texture_bind_command_count
<< ",\"samplerBindCommands\":" << open_gl_plan.sampler_bind_command_count
<< ",\"uploadCommands\":" << open_gl_plan.upload_command_count
<< ",\"mipmapCommands\":" << open_gl_plan.mipmap_command_count
<< ",\"transitionCommands\":" << open_gl_plan.transition_command_count