Skip to content

feature(vulkan): support depth-only render pipelines - #142

Draft
chuzcjoe wants to merge 1 commit into
masterfrom
daily/feature-vulkan-depth-only-passes-20261114
Draft

chuzcjoe wants to merge 1 commit into
masterfrom
daily/feature-vulkan-depth-only-passes-20261114

Conversation

@chuzcjoe

Copy link
Copy Markdown
Collaborator

Problem

VulkanRender could not create a graphics pipeline without a color attachment. On the dynamic-rendering path it threw "Dynamic rendering requires at least one color format". A fragment shader was always required. Depth bias was hard-coded to VK_FALSE. VulkanDynamicRendering always started rendering with exactly one color attachment and a DONT_CARE depth store.

As a result, the library could not express a depth-only pass: no shadow maps, no depth prepass, no rendering into a depth image that is sampled later. The demos had no shadows at all, so the car in CarControlDemo floated over the ground.

Solution

Library (vulkan/):

  • VulkanRender
    • Dynamic-rendering pipelines may now have zero color formats if a depth or stencil format is set. In that case colorAttachmentCount is 0 and the color-blend state has no attachments.
    • Such pipelines may return an empty LoadFragmentShader(), which omits the fragment stage. A pipeline that has a color attachment still requires a fragment shader.
    • New optional virtual SetDepthBias() returns std::optional<DepthBiasInfo{constant_factor, slope_factor}>. The clamp is fixed at 0, so the depthBiasClamp device feature is not needed. Non-finite factors are rejected.
    • Every configuration error is detected before any shader module is created, so the new error paths leak nothing.
    • More than one color format is still rejected; the message now says "at most one".
  • VulkanDynamicRendering::BeginDepthRendering(cmd, depth_view, extent, clear) starts rendering with no color attachments and a depth attachment that clears and then stores. It rejects a null command buffer, a null view, or an empty extent.

Existing subclasses and callers keep their behaviour. The new virtual has a default, and every pipeline that has a color attachment builds the same state as before.

Consumer: CarControlDemo car shadows (examples/vulkan/CarControlDemo/)

  • RenderCarShadow is a depth-only VulkanRender with a vertex shader only, depth bias 1.25 constant / 1.75 slope, and the light matrix passed as a push constant. It owns a 2048² D32_SFLOAT shadow map and a comparison sampler (LESS_OR_EQUAL, opaque-white border, linear filtering only if the format supports it). It reuses the car's existing vertex and index buffers through new read-only accessors on RenderCar.
  • Each frame, Record() does the following:
    1. Transitions the shadow map from UNDEFINED to the depth-attachment layout; the source stage is the previous frame's fragment-shader reads.
    2. Draws the car with BeginDepthRendering.
    3. Transitions the map to SHADER_READ_ONLY for the fragment shader.
  • RenderGround samples the map through sampler2DShadow with 3×3 PCF and darkens shadowed asphalt.
  • main.cpp builds a light projection centred on the car with glm::orthoRH_ZO, using the same direction as the car's key light.

Usage pattern for other callers:

DynamicRenderingInfo info{};
info.depth_format = VK_FORMAT_D32_SFLOAT;
// subclass VulkanRender(context, info): LoadFragmentShader() returns {}, SetDepthBias() optional
dynamic_rendering.BeginDepthRendering(cmd, depth_view, extent, clear);
// bind pipeline, draw
dynamic_rendering.EndDynamicRendering(cmd);

Callers still own layout transitions and synchronization for the depth image, as they already do for every other image in this library.

Candidates considered

Candidate Decision
Depth-only passes plus demo shadows Selected. A real missing capability, it has a visible consumer, and it is verifiable with an exact GPU oracle.
Direct upload into device-local, host-visible memory on unified-memory GPUs (M6 memory type 1 = 0xf; Adreno type 6 = 0xf) Rejected for now. Demo buffers are KB-sized (Galaxy instances are about 1.7 MB), so the startup and memory gain on existing callers is marginal.
AHardwareBuffer import into Vulkan Rejected. Android-only, and nothing in the repo would call it.
Frames in flight Deferred. It depends on open #138 and #139, which rework the same demo loops.

Overlap with open PRs: #137 adds one line in VulkanRender.cpp (passing the pipeline cache) and #137/#141 touch small setup hunks in CarControl main.cpp. Any conflicts should be small and in different places. No shared modules, other backends or new dependencies are touched.

Validation

Final commit 2656b0b. Its tree, 30d1136, is exactly the tree that was built and tested on both platforms (diff sha256 7f3a0af3…). Both platforms were built from clean build directories. Base is origin/master 0dc12da, freshly fetched before validation and again before commit.

macOS native (Apple M6 / MoltenVK, macOS 27.0.1, Xcode SDK MacOSX27.2, Vulkan SDK 1.4.350.0, core variant with -Wall -Wextra -Wpedantic -Werror):

source /Users/joechu/VulkanSDK/1.4.350.0/setup-env.sh
./scripts/run.sh -t macos -r tests
  • Exit 0, 6 executables, 63 passed / 0 failed / 0 skipped (mat 3, threadpool 4, stb 4, opencl 8, vulkan 43, metal 1).
  • The 43 vulkan tests include the 3 new ones. Zero compiler warnings.
  • The Khronos validation layer is loaded (confirmed with VK_LOADER_DEBUG=layer), and no validation messages appeared.
  • Demo smoke: vk_car_control_demo ran several times with validation on and produced 0 messages. A frame captured on the 120th present (through a scratch present-hook shim, not part of this PR) shows the soft car shadow on the ground, offset toward −X/−Z as the light direction predicts. The capture was repeated after the review fix to the light projection.

Android arm64-v8a physical device: Xiaomi 2210132C (nuwa, qcom / Adreno 740), Android 13 / API 33. adb devices -l listed exactly one authorized device, 1dda2807; ANDROID_SERIAL was pinned to it and ro.kernel.qemu is empty (not an emulator). NDK 28.2.13676358 (r28c), target API 34.

export ANDROID_NDK_ROOT=$ANDROID_SDK_ROOT/ndk/28.2.13676358
./scripts/run.sh -t arm64-v8a -r tests
  • Exit 0, 7 executables actually ran on the device, 65 passed / 0 failed / 0 skipped (mat 3, threadpool 4, stb 4, opencl 8, vulkan 43, gles 2, ahardwarebuffer 1).
  • All 3 VulkanDepthOnlyRenderTest cases reported OK on the device. Zero compiler warnings.
  • Adreno prints its vendor advisory VKDBGUTILWARN003; this is not a validation message.

New tests (tests/vulkan/VulkanDepthOnlyRenderTest.cpp): a 32×32 D32_SFLOAT image, a pipeline with no color attachment and no fragment stage, and depth read back with vkCmdCopyImageToBuffer.

  • Writes and depth test: three scissored draws must produce exact per-texel values (0.25 kept, 0.5 written, a farther 0.75 rejected). A second pass shows the clear to 1.0 and that the store persists.
  • Depth bias:
    • Constant factor: the depth increase must be positive, uniform across the image, and at most 4096·2⁻²² (the nominal value is 4096·2⁻²⁴).
    • Slope factor 2 on a ramp of 0.0125 per pixel: the bias must be 0.025 ± 5% at every texel, and the unbiased ramp must match the analytic per-pixel-centre depth within 1e-5.
  • Rejection: no attachment at all; a color attachment without a fragment shader; an empty vertex shader; infinite or NaN bias; two color formats; and BeginDepthRendering with a null view, null command buffer, or zero width or height.

Review gate: independent GPT 6.1 sol, high effort, run read-only and ephemeral.

  • Round 1 had two findings, both fixed:
    • R1-1: GLM was included before GLM_FORCE_DEPTH_ZERO_TO_ONE, so the light projection used a −1..1 depth range. Fixed with an explicit orthoRH_ZO.
    • R1-2: positional aggregate initializers. Replaced with named members.
  • Round 2: both findings marked resolved, no new findings, GATE: PASS.

Limitations

  • The demo is macOS-only (GLFW). Its shadow was checked visually from one captured frame plus clean validation runs; there is no automated image assertion.
  • The bias tolerances follow the spec definition for floating-point depth formats and were confirmed on Apple M6 and Adreno 740 only.
  • Stencil-only rendering is allowed by the pipeline check but not exercised.
  • No performance claims are made.

Changes

git diff --numstat origin/master: +782 / −28 = 810 lines.

  • Library: +98 / −18
    • vulkan/include/VulkanRender.h +7
    • vulkan/src/VulkanRender.cpp +50 / −18
    • vulkan/include/VulkanDynamicRendering.h +2
    • vulkan/src/VulkanDynamicRendering.cpp +39
  • Example consumer: +324 / −10
    • RenderCarShadow.h/.cpp +240 (new)
    • CarShadow.vert +11 (new)
    • Ground.vert/.frag +22
    • RenderGround.h/.cpp, RenderCar.h/.cpp, main.cpp: the remainder
  • Tests: +360
    • VulkanDepthOnlyRenderTest.cpp +347
    • DepthOnly.vert +13
  • Docs / benchmarks: 0

Risk

  • Compatibility: additive. A subclass that returned an empty fragment shader together with a color attachment used to fail inside the driver and now gets std::invalid_argument before any Vulkan objects are created. The error message for more than one color format changed wording ("at most one").
  • Demo: CarControlDemo now records one extra depth pass per frame (a 2048² D32 image, about 16 MiB) and binds a third ground descriptor. RenderGround::Init and UpdateUniformBuffer gained shadow parameters; their only caller is this demo.
  • Formats: the demo checks that D32_SFLOAT supports depth attachment plus sampling and throws a clear error otherwise. The tests skip if the queue cannot record graphics work or if D32_SFLOAT cannot be copied.

🤖 Generated with Claude Code

Allow dynamic-rendering pipelines with no color attachment when a depth
or stencil format is set, make the fragment stage optional for them, and
add configurable depth bias plus VulkanDynamicRendering::BeginDepthRendering.
CarControlDemo uses this to render the car into a shadow map and shade
the ground with comparison-sampled shadows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant