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 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 enum tokens before a real GL context is needed. It also plans whole recorded
command streams, preserving per-command planned data while counting render command streams, preserving per-command planned data while counting render
passes, draws, shader binds, shader uniforms, texture uploads, mipmap passes, draws, shader binds, shader uniforms, texture/sampler binds, texture
generation, texture transitions, texture copies, texture readbacks, frame uploads, mipmap generation, texture transitions, texture copies, texture
captures, passthrough commands, trace commands, unsupported commands, and readbacks, frame captures, passthrough commands, trace commands, unsupported
render-pass ordering errors such as state changes outside a pass, nested commands, and render-pass ordering errors such as state changes outside a
passes, and unclosed passes. It also validates executable command pass, nested passes, and unclosed passes. It also validates executable
dependencies, including shader-before-uniform and shader-plus-mesh before command dependencies, including shader-before-uniform and shader-plus-mesh
draw within each render pass. 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, Desktop VR drawing also consumes backend-owned scissor/depth/blend state,
depth clear masks, active texture units, and fallback 2D texture unbind depth clear masks, active texture units, and fallback 2D texture unbind
targets while retaining the existing VR SDK/platform bridge shape. 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 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 rejection before a runtime GL context is needed. It also plans whole recorded
command streams, preserving per-command planned data while counting render command streams, preserving per-command planned data while counting render
passes, draws, shader binds, shader uniforms, texture uploads, mipmap passes, draws, shader binds, shader uniforms, texture/sampler binds, texture
generation, texture transitions, texture copies, texture readbacks, frame uploads, mipmap generation, texture transitions, texture copies, texture
captures, passthrough commands, trace commands, unsupported commands, and readbacks, frame captures, passthrough commands, trace commands, unsupported
render-pass ordering errors such as state changes outside a pass, nested commands, and render-pass ordering errors such as state changes outside a pass,
passes, and unclosed passes. It also validates executable command dependencies, nested passes, and unclosed passes. It also validates executable command
including shader-before-uniform and shader-plus-mesh before draw within each dependencies, including shader-before-uniform and shader-plus-mesh before draw
render pass. within each render pass, and rejects invalid texture/sampler bind slots in
malformed recorded streams.
The existing renderer classes are not yet fully The existing renderer classes are not yet fully
behind the renderer interfaces. behind the renderer interfaces.

View File

@@ -191,12 +191,16 @@ OpenGlPlannedCommand plan_recorded_render_command(pp::renderer::RecordedRenderCo
case pp::renderer::RecordedRenderCommandKind::bind_texture: case pp::renderer::RecordedRenderCommandKind::bind_texture:
planned.kind = OpenGlPlannedCommandKind::bind_texture; planned.kind = OpenGlPlannedCommandKind::bind_texture;
planned.texture_format = texture_format_for_renderer_format(command.texture_desc.format); 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; break;
case pp::renderer::RecordedRenderCommandKind::bind_sampler: case pp::renderer::RecordedRenderCommandKind::bind_sampler:
planned.kind = OpenGlPlannedCommandKind::bind_sampler; planned.kind = OpenGlPlannedCommandKind::bind_sampler;
planned.sampler = sampler_state_for_renderer_sampler_desc(command.sampler_desc); 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; break;
case pp::renderer::RecordedRenderCommandKind::bind_mesh: case pp::renderer::RecordedRenderCommandKind::bind_mesh:
planned.kind = OpenGlPlannedCommandKind::bind_mesh; planned.kind = OpenGlPlannedCommandKind::bind_mesh;
@@ -346,6 +350,18 @@ OpenGlCommandPlan plan_recorded_render_commands(
record_dependency_error(plan, index); record_dependency_error(plan, index);
} }
break; 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: case OpenGlPlannedCommandKind::bind_mesh:
if (!in_render_pass) { if (!in_render_pass) {
record_render_pass_order_error(plan, index); record_render_pass_order_error(plan, index);

View File

@@ -53,6 +53,8 @@ struct OpenGlPlannedCommand {
pp::renderer::ReadbackRegion readback_region; pp::renderer::ReadbackRegion readback_region;
pp::renderer::ReadbackRegion source_region; pp::renderer::ReadbackRegion source_region;
pp::renderer::ReadbackRegion destination_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::uint64_t upload_bytes = 0;
std::uint32_t generated_mip_levels = 0; std::uint32_t generated_mip_levels = 0;
std::uint64_t generated_mip_bytes = 0; std::uint64_t generated_mip_bytes = 0;
@@ -79,6 +81,8 @@ struct OpenGlCommandPlan {
std::uint32_t draw_command_count = 0; std::uint32_t draw_command_count = 0;
std::uint32_t shader_bind_command_count = 0; std::uint32_t shader_bind_command_count = 0;
std::uint32_t uniform_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 upload_command_count = 0;
std::uint32_t mipmap_command_count = 0; std::uint32_t mipmap_command_count = 0;
std::uint32_t transition_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) COMMAND pano_cli record-render --width 32 --height 16)
set_tests_properties(pano_cli_record_render_smoke PROPERTIES set_tests_properties(pano_cli_record_render_smoke PROPERTIES
LABELS "renderer;integration;desktop-fast" 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 add_test(NAME pano_cli_record_render_exercises_clear_reset
COMMAND pano_cli record-render --width 32 --height 16 --exercise-clear) 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 }, .extent = pp::renderer::Extent2D { .width = 16U, .height = 16U },
.format = pp::renderer::TextureFormat::depth24_stencil8, .format = pp::renderer::TextureFormat::depth24_stencil8,
}, },
.texture_slot = 3U,
}); });
const auto sampler = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand { const auto sampler = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand {
.kind = pp::renderer::RecordedRenderCommandKind::bind_sampler, .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_v = pp::renderer::SamplerAddressMode::mirrored_repeat,
.address_w = pp::renderer::SamplerAddressMode::clamp_to_border, .address_w = pp::renderer::SamplerAddressMode::clamp_to_border,
}, },
.sampler_slot = 3U,
}); });
const auto mesh = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand { const auto mesh = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand {
.kind = pp::renderer::RecordedRenderCommandKind::bind_mesh, .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.supported);
PP_EXPECT(h, texture.requires_render_pass); PP_EXPECT(h, texture.requires_render_pass);
PP_EXPECT(h, texture.texture_format.internal_format == 0x88F0U); 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.supported);
PP_EXPECT(h, sampler.sampler.min_filter == 0x2702U); PP_EXPECT(h, sampler.sampler.min_filter == 0x2702U);
PP_EXPECT(h, sampler.sampler.wrap_t == 0x8370U); PP_EXPECT(h, sampler.sampler.wrap_t == 0x8370U);
PP_EXPECT(h, sampler.sampler_slot == 3U);
PP_EXPECT(h, mesh.supported); PP_EXPECT(h, mesh.supported);
PP_EXPECT(h, mesh.primitive_mode == 0x0005U); PP_EXPECT(h, mesh.primitive_mode == 0x0005U);
PP_EXPECT(h, draw.supported); 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), .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 { const auto bad_mesh = pp::renderer::gl::plan_recorded_render_command(pp::renderer::RecordedRenderCommand {
.kind = pp::renderer::RecordedRenderCommandKind::draw, .kind = pp::renderer::RecordedRenderCommandKind::draw,
.mesh_desc = pp::renderer::MeshDesc { .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_depth.depth.compare_function == 0U);
PP_EXPECT(h, !bad_sampler.supported); PP_EXPECT(h, !bad_sampler.supported);
PP_EXPECT(h, bad_sampler.sampler.wrap_s == 0U); 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.supported);
PP_EXPECT(h, bad_mesh.primitive_mode == 0U); PP_EXPECT(h, bad_mesh.primitive_mode == 0U);
PP_EXPECT(h, !bad_blit.supported); 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.draw_command_count == 1U);
PP_EXPECT(h, plan.shader_bind_command_count == 1U); PP_EXPECT(h, plan.shader_bind_command_count == 1U);
PP_EXPECT(h, plan.uniform_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.passthrough_command_count == 0U);
PP_EXPECT(h, plan.trace_command_count == 1U); PP_EXPECT(h, plan.trace_command_count == 1U);
PP_EXPECT(h, plan.unsupported_command_count == 0U); 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); 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) void flags_broken_render_pass_command_order(pp::tests::Harness& h)
{ {
const std::vector<pp::renderer::RecordedRenderCommand> outside_pass { 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("rejects_unsupported_command_tokens", rejects_unsupported_command_tokens);
harness.run("names_planned_command_kinds", names_planned_command_kinds); harness.run("names_planned_command_kinds", names_planned_command_kinds);
harness.run("plans_valid_recorded_command_streams", plans_valid_recorded_command_streams); 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_broken_render_pass_command_order", flags_broken_render_pass_command_order);
harness.run("flags_missing_render_dependencies", flags_missing_render_dependencies); harness.run("flags_missing_render_dependencies", flags_missing_render_dependencies);
harness.run("tracks_unsupported_commands_in_streams", tracks_unsupported_commands_in_streams); 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 << ",\"drawCommands\":" << open_gl_plan.draw_command_count
<< ",\"shaderBindCommands\":" << open_gl_plan.shader_bind_command_count << ",\"shaderBindCommands\":" << open_gl_plan.shader_bind_command_count
<< ",\"uniformCommands\":" << open_gl_plan.uniform_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 << ",\"uploadCommands\":" << open_gl_plan.upload_command_count
<< ",\"mipmapCommands\":" << open_gl_plan.mipmap_command_count << ",\"mipmapCommands\":" << open_gl_plan.mipmap_command_count
<< ",\"transitionCommands\":" << open_gl_plan.transition_command_count << ",\"transitionCommands\":" << open_gl_plan.transition_command_count