Skip to content

feature(vulkan): back transient attachments with lazily allocated memory - #141

Draft
chuzcjoe wants to merge 1 commit into
masterfrom
daily/feature-vulkan-lazy-transient-attachments-20261113
Draft

chuzcjoe wants to merge 1 commit into
masterfrom
daily/feature-vulkan-lazy-transient-attachments-20261113

Conversation

@chuzcjoe

Copy link
Copy Markdown
Collaborator

Problem

Tile-based GPUs keep transient attachments (MSAA color and depth that are cleared at the start of a pass and discarded at the end) in on-chip tile memory. Vulkan exposes this through VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT plus a VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT memory type. MoltenVK on Apple GPUs maps that type to Metal memoryless storage, and Adreno exposes it as well. Both validation devices report it (DEVICE_LOCAL | LAZILY_ALLOCATED, flags 0x11).

On master, VulkanImage always picks the first type matching the requested properties, which is plain DEVICE_LOCAL (0x1). As a result:

  • The MSAA color image in MultiSamplingDemo already sets TRANSIENT_ATTACHMENT usage, but still gets fully backed memory.
  • The swapchain depth attachment (VulkanSwapChain::CreateDepthResources, used by DepthTesting/LoadModel/DrawTexture) and the depth attachments created by MultiSampling/Physics/FPSShooter/CarControl are fully backed too. All of them use loadOp = CLEAR and storeOp = DONT_CARE and are never sampled or copied.

Measured on master: 4 sets of 4x MSAA RGBA8 color plus 4x D32 depth at 1080p cost 388.5 MiB of Metal device memory on macOS and 258 MiB of per-process GPU memory on Android. That memory holds data that is never read back.

Solution

  • VulkanImage: when usage contains VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT, choose the first memory type in the image's memoryTypeBits that has the requested properties plus LAZILY_ALLOCATED. If there is none, fall back to the previous FindMemoryType(properties) choice, so devices without lazy memory behave exactly as before. The chosen type's property flags are stored, exposed as VulkanImage::memory_properties(), and carried over by move assignment. Images without TRANSIENT usage are unaffected (the spec excludes lazily allocated types from their memoryTypeBits).
  • Callers: VulkanSwapChain::CreateDepthResources and the depth images in MultiSampling/Physics/FPSShooter/CarControl now add TRANSIENT_ATTACHMENT usage. The MultiSampling MSAA color image benefits without any change. Every affected attachment uses CLEAR load and DONT_CARE store (VulkanRenderPass depth, and VulkanDynamicRendering depth plus resolved MSAA color), which is what memoryless storage requires.
  • Usage: create attachments that are cleared and discarded every pass with TRANSIENT_ATTACHMENT usage. VulkanImage picks lazily allocated memory automatically, and memory_properties() reports what was selected.

Alternatives considered:

  • Requiring callers to pass LAZILY_ALLOCATED explicitly. Rejected: it makes callers handle missing support, and the transient usage bit already states the intent.
  • A separate allocation API. Rejected as unnecessary surface area for one selection rule.
  • Changing VulkanContext::FindMemoryType. Rejected because buffers and other callers use it too.

Validation

GPT-6.1-sol (high) review gate: 3 rounds, all findings fixed, final GATE: PASS. The findings covered the compute-only queue family check, separating unavailable telemetry from 0, removing comments, and checking D32 4x format support in the benchmark.

macOS (Apple M6, macOS 27.0.1, Vulkan SDK 1.4.350.0 / MoltenVK, validation layer enabled in the new tests):

  • ./scripts/run.sh -t macos -r tests → exit 0. 6 test executables, 63 passed / 0 failed / 0 skipped (vulkan_tests 43, including the 3 new tests).
  • Demo smoke runs for 5 s each with validation: vk_multi_sampling_demo, vk_physics_demo, vk_fps_demo, vk_car_control_demo, vk_depth_demo, vk_model_demo, vk_texture_demo all ran until stopped, with 0 validation messages.

Android (only authorized physical device 2210132C (nuwa, Qualcomm Adreno 740), Android 13 / API 33, NDK 28.2.13676358, arm64-v8a, android-34):

  • ./scripts/run.sh -t arm64-v8a -r tests → exit 0. 7 test executables ran on the device, 65 passed / 0 failed / 0 skipped (vulkan_tests 43, including the 3 new tests).

New tests (VulkanTransientAttachmentTest):

  • Transient 4x color and depth images get DEVICE_LOCAL | LAZILY_ALLOCATED exactly when the image's memoryTypeBits offer such a type (both devices: 0x11). memory_properties() survives move assignment.
  • Persistent images keep 0x1.
  • A 4x MSAA render through transient color and depth, using the depth test plus a scissored draw, resolves to the exact expected pixels. A second frame proves that both attachments are cleared again (depth from the previous frame does not survive).

Resource benchmark vulkan_transient_attachment_benchmark. Setup: 4 sets of 4x RGBA8 color plus 4x D32 depth, each cleared in one pass, same process. 1 warm-up per mode, then 7 rounds alternating mode order, reporting median/min/max. 3 independent invocations each of a baseline binary built from master's VulkanImage.cpp and the final binary. Metric: device-local heap usage from VK_EXT_memory_budget on macOS; per-process GPU memory from dumpsys gpu --gpumem on Android, where Adreno does not expose the memory budget extension. Every sample had min == max, and all 3 invocations matched.

Platform Extent Image size Master: usage of TRANSIENT images This PR
macOS M6 1280x720 117.00 MiB 175.50 MiB 0.00 MiB
macOS M6 1920x1080 259.00 MiB 388.50 MiB 0.00 MiB
macOS M6 2560x1440 458.00 MiB 687.00 MiB 0.00 MiB
Adreno 740 1280x720 112.97 MiB 113.94 MiB 0.97 MiB
Adreno 740 1920x1080 256.06 MiB 258.08 MiB 2.02 MiB
Adreno 740 2560x1440 451.81 MiB 455.44 MiB 3.62 MiB

Persistent images (no TRANSIENT usage) are identical in both binaries. Limitations:

  • This is a resource saving. Frame time was not measured.
  • The demos are desktop GLFW programs and only run on macOS. On Android, the library path is covered by the gtest and the benchmark.
  • MoltenVK's vkGetDeviceMemoryCommitment reports the full allocation size, so heap usage is used as the metric instead.
  • Results apply only to the two devices measured.

Changes

12 files, +687 / −12 (699 lines):

  • Implementation: vulkan/include/VulkanImage.h +3, vulkan/src/VulkanImage.cpp +21/−2, vulkan/src/VulkanSwapChain.cpp +2/−2, plus demos MultiSamplingDemo/main.cpp +4/−2, PhysicsDemo/main.cpp, FPSShooterDemo/main.cpp, CarControlDemo/main.cpp +3/−2 each. Subtotal +39/−12.
  • Tests/benchmark: tests/vulkan/VulkanTransientAttachmentTest.cpp +385, TransientAttachment.vert +11, TransientAttachment.frag +10, benchmarks/TransientAttachmentBenchmark.cpp +239, tests/vulkan/CMakeLists.txt +3. Subtotal +648/−0.
  • Docs: 0.

Risk

  • API compatible: the new accessor is additive, and images without TRANSIENT usage take exactly the old path.
  • Callers that set TRANSIENT_ATTACHMENT on an image they later load, store, sample, or copy were already violating the usage contract. With lazily allocated memory, such misuse now shows up as lost contents instead of working by accident. All in-tree users were audited.
  • Overlap with open PRs: feature(vulkan): recreate swapchains when window surfaces resize #139 adds swapchain-resize code that re-creates the same demo depth images (a small textual conflict in those main.cpp files; the re-created images should also get TRANSIENT usage). feature(vulkan): persist validated pipeline caches across pipelines #137 touches other lines of CarControl/FPS/Physics main.cpp.
  • Devices without a lazily allocated type fall back to the previous selection, which is covered by the selection test's expectation.

🤖 Generated with Claude Code

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