CNA-BUG-113: An enabled zero-area ScissorRectangle draws unclipped on VULKAN and SDL_GPU, while WEBGPU clips everything and METAL suppresses the draw

CNA snapshot 009d40f5  ·  Known Issues › Current bugs  ·  source links pinned to 009d40f5

✓

Evidence basis: source-verified at the pinned commit; recorded by CNA's own run (not repeated here); tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Nothing on this page was executed unless the Evidence section says so.

Vulkan's computeScissor and SDL_GPU's ApplyScissorForRef treat a zero width or height like a disabled scissor and use the full target; XNA's contract (rasterise only inside the rectangle) means nothing is drawn.

Identifier
CNA-BUG-113
Category
Bug
Subsystem
Graphics & renderers
Status
Open
Verified against
CNA 009d40f5 (009d40f5dd085c4e674d3479675fac84b12b3e0a)
Severity
Medium (a triage suggestion, not a project priority)
Evidence basis
Recorded by CNA: CNA's own recorded run, not repeated here
Tests touching this area
Yes: see Current tests
Affected contract
GraphicsDevice.ScissorRectangle with zero width or height and RasterizerState.ScissorTestEnable = true

Expected behaviour

With the scissor test enabled, only pixels inside the rectangle are written; an empty rectangle writes nothing (Direct3D 9, which XNA runs on, and WEBGPU and METAL in CNA behave this way).

Actual behaviour at TARGET

VulkanRenderer.cpp's computeScissor returns the full framebuffer when !enabled || sw == 0 || sh == 0; SdlGpuRenderer.cpp's ApplyScissorForRef takes its full-target branch unless scissorEnabled && scissorW > 0 && scissorH > 0. WebGPURenderer.cpp's ApplyDrawScissor clips an enabled empty rectangle to nothing, and METAL suppresses the draw (MetalRasterState::ShouldSkipDraw). So a UI panel whose clip rectangle collapses to zero height draws its whole content over the screen on two renderers and disappears on the others. CNA already records this: the shared fixture deferred_scissor_capture_test.cpp declares emptyScissorDrawsNothing=false for VULKAN and SDL_GPU ('recorded as its own finding rather than fixed here'), and its check E1 (3D draws into a render target) asserts the unclipped output for those two, so the wrong result is pinned as expected output.

Source locations

Evidence

Checked by reading the named sources at 009d40f5; nothing was built or executed for this entry. The METAL behaviour is taken from the chapter package's reading; not re-read.

Independent re-verification: Checked by reading the named sources at 009d40f5; nothing was built or executed for this entry. The METAL behaviour was read in MetalRasterState (NativeScissor, ShouldSkipDraw) and metal-renderer.md. CNA's own records agree: deferred_scissor_capture_test.cpp, registered as Vulkan_Deferred_Scissor and SdlGpu_Deferred_Scissor, sets zero-width, zero-height and 0x0 scissors in check E1 and declares that VULKAN and SDL_GPU do not clip them, while the other families in that fixture (WEBGPU, EASYGL, OPENGL4, SOFTWARE, HEADLESS, DIRECTX9, DIRECTX11, DIRECTX12) declare that a degenerate rectangle rasterizes nothing; remediation/REMEDIATION_PROGRESS.md records the same divergence as an independent finding that was not fixed. So the existing tests do use empty rectangles (on the 3D render-target path; the SpriteBatch path with an empty rectangle is not exercised), but they assert the divergent result.

Independent re-verification: Checked by reading the named sources at 009d40f5; nothing was built or executed for this entry. The METAL behaviour was read in MetalRasterState (NativeScissor, ShouldSkipDraw) and metal-renderer.md. CNA's own records agree: deferred_scissor_capture_test.cpp, registered as Vulkan_Deferred_Scissor and SdlGpu_Deferred_Scissor, sets zero-width, zero-height and 0x0 scissors in check E1 (3D draws into a render target) and declares through emptyScissorDrawsNothing=false that VULKAN and SDL_GPU do not clip them, while the other families in that fixture (WEBGPU, EASYGL, OPENGL4, SOFTWARE, HEADLESS, DIRECTX9, DIRECTX11, DIRECTX12) declare that a degenerate rectangle rasterizes nothing; remediation/REMEDIATION_PROGRESS.md records the same divergence as an independent finding that was not fixed. So the existing tests do use empty rectangles (on the 3D render-target path; the SpriteBatch path with an empty rectangle is not exercised) but assert the divergent result. Current tests, corrected: Vulkan_Deferred_Scissor and SdlGpu_Deferred_Scissor exercise empty rectangles in E1 and pin the unclipped output for these two renderers; fixing the renderers means flipping those declarations.

Focused reproduction

// Illustrative; not compiled or run for this entry.
device.setScissorRectangleProperty(Rectangle(10, 10, 0, 50));   // collapsed panel
batch.Begin(SpriteSortMode::Deferred, nullptr, nullptr, nullptr, scissorOn);
batch.Draw(panelContent, Rectangle(0, 0, 800, 600), Color::White);
batch.End();
// Expected: nothing drawn. VULKAN / SDL_GPU: full-screen panel content.

Current tests

Vulkan_Deferred_Scissor and SdlGpu_Deferred_Scissor (deferred_scissor_capture_test.cpp, check E1) draw 3D geometry into a render target with zero-width, zero-height and 0×0 scissors and pin the unclipped output for these two renderers (emptyScissorDrawsNothing=false); the other families in that fixture declare that a degenerate rectangle rasterizes nothing. The SpriteBatch path with an empty rectangle is not exercised.

Regression test

Flip the two emptyScissorDrawsNothing declarations for VULKAN and SDL_GPU once the renderers clip a degenerate rectangle: the fixture then fails until they do.

Blast radius

Clipped UI (scroll views, collapsing panels, animated reveals) that can reach zero size on VULKAN or SDL_GPU.

Workaround

Skip drawing when the clip rectangle is empty.

The same subject is explained at several altitudes. These are the neighbouring pages at each one.

Known issues
Bug index