Lifetimes derive from the real dependency ordering (retained DAG + compile-time happens-before closure), not command-index intervals. Aliasing is restricted to the single-queue contract; alias reuse is synchronized with memory/image barriers anchored at first-use commands. - renderer_device: aliasable shared allocations (CAN_ALIAS), memory- requirements queries incl. dedicated hints, offset-aware bind_buffer/image_memory2, grouped bind under one allocator lock - renderer_resources: unallocated resource creation, requirements cached in Allocation::Unmanaged (OnceLock), MemoryGroup pooling with intersecting memoryTypeBits, bind-time validation - renderer_rendergraph: retained dependency DAG, happens_before closure, batch lifetimes, Placement type, plan_placements with pooled aliasing (first-fit over offset-sorted ranges, pool classes split by bufferImageGranularity), alias-barrier construction, placement/bind error variants - perf: quadratic compile/resolve paths eliminated (per-resource indexing, frontier caching, backward-scan prune, cached recording index, recorder scratch reuse); single requirements query per resource through plan/validate/bind - design plan + GPU-gated validation tests
131 lines
21 KiB
Markdown
131 lines
21 KiB
Markdown
# renderer
|
|
|
|
## Purpose
|
|
`renderer` builds rendering orchestration, command submission, swapchains, and render-graph APIs on `renderer_resources` and the lower-level `renderer_device` layer. It re-exports selected `renderer_device` APIs and uses crate-private aliases to the resource crate. Follow [`../renderer_device/AGENTS.md`](../renderer_device/AGENTS.md) for device ownership internals and [`../renderer_resources/AGENTS.md`](../renderer_resources/AGENTS.md) for resource construction and ownership.
|
|
|
|
## Module map
|
|
- `lib.rs`: public root API, `Renderer2`, sampler cache, egui state, texture manager, and `renderer_device` re-exports.
|
|
- `memory.rs` (not declared by `lib.rs`): empty; not part of the compiled crate module tree.
|
|
- `renderer_resources::{buffers, images, pipeline}`: public resource-crate modules aliased through `pub(crate) use` in this crate root. These aliases preserve internal `crate::buffers::X`, `crate::images::X`, and `crate::pipeline::X` paths without exposing public renderer modules.
|
|
- `commands.rs` (public): command pools/buffers, command recording helpers, queue submission, and command futures.
|
|
- `swapchain.rs` (public): `Surface`, swapchain configuration, acquired images/frames, and presentation.
|
|
- `render_graph/mod.rs` (public): re-exports the legacy graph API from `legacy.rs`; sibling `commands.rs`, `graph_builder.rs`, `recorder.rs`, and `resources.rs` are private modules.
|
|
- `render_graph/legacy.rs`: public resource/pass descriptions, graph construction/resolution, resource barriers, and `RenderContext`.
|
|
- `render_graph/commands.rs`: private typed render-graph commands and resource-access declarations; not wired into the legacy graph.
|
|
- `render_graph/graph_builder.rs`: private newer graph-builder scaffolding and API example.
|
|
- `render_graph/recorder.rs`: private typed command-list and side-effect tracking scaffolding.
|
|
- `render_graph/resources.rs`: graph resource IDs, buffer/texture ranges and access scopes; private.
|
|
- `rendering/mod.rs` (public): `Wireframe` helper and re-exports `egui_pass` / `egui_pre_pass`.
|
|
- `egui.rs` is included as private `egui_pass`: egui texture upload/pre-pass and drawing integration.
|
|
- `egui_pass` is a private module, but `rendering` publicly re-exports its `egui_pass` and `egui_pre_pass` functions.
|
|
- `text.rs` (private): font storage, glyph rasterization, and CPU-side glyph-atlas data; largely internal/dead-code-allowed.
|
|
- `util.rs` (public): renderer helpers and stable re-exports of `eq_f32`, `ord_f32`, `hash_f32`, and `F32` from `renderer_resources::util`; `weak_vec` moved there and is not re-exported from renderer.
|
|
- `texture` (private inline module in `lib.rs`): `TextureManager` and `TextureId` around shared images.
|
|
|
|
## Key types and API surface
|
|
- Public modules are `commands`, `render_graph`, `rendering`, `swapchain`, and `util`; `text`, `egui_pass`, and `texture` are private. `buffers`, `images`, and `pipeline` are crate-private aliases to public `renderer_resources` modules, not public paths through `renderer`.
|
|
- `lib.rs` re-exports `ash` and the principal `renderer_device` APIs at crate root, including `device`, `instance`, `queue`, `sync`, `Device`, `DeviceOwned`, `DeviceRef`, `Instance`, `Error`, and `Result`.
|
|
- Root entry point: `Renderer2` owns a `Device`, `SamplerCache`, `TextureManager`, and display handle. Its API creates surfaces and builds/submits a `RenderGraph` in `draw_graph`.
|
|
- Root helper types include `EguiState`, `EguiTextureInfo`, and `SamplerCache`. `TextureManager` and `TextureId` are used by these internals but are not re-exported at crate root.
|
|
- `SamplerCache` caches `renderer_resources::pipeline::Sampler` objects by `SamplerDesc`; `EguiState` keeps texture mappings and descriptor/pipeline resources for egui.
|
|
- `commands::CommandPools`, `CommandPool`, `CommandBuffer`, and `CommandBufferFuture` provide per-thread command-pool allocation and submission. `SingleUseCommandPool`/`SingleUseCommand` support one-shot operations.
|
|
- `commands::CommandBufferExt` adds recording helpers for barriers, copies, rendering, and pipeline state. `CommandList<T: HasQueue>` submits command buffers that share a queue.
|
|
- `swapchain::Surface` owns the surface and current configured swapchain; `SwapchainConfiguration` describes image format, extent, present mode, usage, and image count. Acquired image values must be presented.
|
|
- Public `render_graph` items come from `legacy.rs`: `RenderGraph`, `GraphResourceId`, `GraphResourceDesc`, `PassDesc`, `RenderContext`, `Access`, `LoadOp`, and `StoreOp` provide pass/resource declaration and resolution.
|
|
- `RenderGraph::add_resource` / `import_image` / `import_buffer` / `import_framebuffer` register graph resources; `add_pass` records read/write declarations and a callback; `resolve` creates internal resources and returns a command list with graph transitions.
|
|
- `rendering::Wireframe` is an example higher-level renderer; egui pass functions are re-exported from `rendering` rather than the private `egui_pass` module.
|
|
- Resource types are defined in `renderer_resources`: `Buffer`/`BufferDesc`; `Image`/`ImageDesc`, `ImageView`/`ImageViewDesc`, `MipRange`, `Extent`, and `Offset`; and pipeline, descriptor, shader, and sampler types.
|
|
- Buffer/image descriptors carry Vulkan usage, `renderer_device::device::MemoryLocation`, queue-sharing flags, and allocation strategy; `Buffer::map`/`map_mut` expose mapped memory for VMA-mapped allocations (CPU-mapped locations require host-visible, host-coherent memory).
|
|
- The egui integration uploads texture deltas to GPU images and records passes through the legacy `RenderGraph`; shader binaries remain under `crates/renderer/shaders`, and renderer's hardcoded shader paths are unchanged.
|
|
- `ImageDesc` includes image shape, format, layout, usage, queue family selection, and allocation options. The resource crate's `Image::new_with_allocation` checks allocation size and memory-type compatibility before binding.
|
|
- `ImageViewDesc` selects view type, format, aspects, mip range, and array layers; the resource crate's `Image::create_view` normalizes `REMAINING_MIP_LEVELS` / `REMAINING_ARRAY_LAYERS` and checks image/view compatibility.
|
|
- `BufferDesc` and `ImageDesc` use `renderer_device::device::QueueFlags` to select concurrent sharing families. Empty/single-family selections use exclusive sharing.
|
|
- `Pipeline` retains its `PipelineLayout` via `Arc`; `DescriptorPool` serializes descriptor allocation/reset using a mutex. Pipeline creation uses the device pipeline cache.
|
|
- `Surface::configure` skips recreating an equivalent swapchain for the same device; configuration validation falls back for unsupported present mode/format and rejects zero or oversized extents.
|
|
- `SwapchainImage` dereferences to an `ImageView`; `SwapchainFrame` exposes the swapchain, image, format, view, and acquire/release semaphores used by frame submission.
|
|
- `render_graph::legacy::RenderGraph` stores resource descriptions until `resolve`; framebuffer imports are marked as present outputs.
|
|
- `render_graph::legacy::PassDesc` lists per-pass `reads`, `writes`, and an optional `RecordFn`; the graph resolver builds dependencies and transitions between `Access` states.
|
|
- `render_graph::commands::Command` distinguishes side-effect declaration from GPU command application, with commands marking resource ranges/stages/accesses for barriers.
|
|
- `render_graph::recorder::CommandList` type-erases recorded commands into bytes and tracks per-resource side effects.
|
|
- Internal text support defines `FontId` and a private font atlas store; it rasterizes glyphs with `cosmic-text` into CPU image data but is not a public renderer API.
|
|
- `util::DropGuard`, `MutexExt`, `FormatExt`, `Rgba`/`Rgba8`, and `def_monotonic_id!` support lifetime callbacks, lock closures, format conversion, color values, and IDs.
|
|
- `render_graph::legacy::RenderContext` exposes the selected device, one-shot command buffer, graph resources, and optional framebuffer to each pass record callback.
|
|
- Graph `Access` includes stage, access mask, and image layout. Imported resources require callers to describe their initial access; framebuffer import marks presentation as an output.
|
|
- `DeviceObject<vk::Buffer>`, `DeviceObject<vk::Image>`, `DeviceObject<vk::ImageView>`, and `DeviceObject<vk::SwapchainKHR>` wrap device-owned Vulkan handles. Resource wrappers retain allocations/devices as needed; see the device-layer guide for lifecycle rules.
|
|
- `renderer::device::Allocation` (from `renderer_device`) distinguishes owned, shared, and unmanaged GPU allocations; the resource image implementation uses it when constructing allocated images.
|
|
- `GraphResource` can hold imported resources, owned buffers/images, framebuffers, or resource descriptions awaiting resolution. `RenderGraph::resolve` creates described resources and records `SingleUseCommand`s for its passes.
|
|
- Descriptor, pipeline, shader, and sampler abstractions are implemented in `renderer_resources::pipeline`. Root `SamplerCache` uses them through the private alias, not a public renderer pipeline module.
|
|
|
|
## Invariants and gotchas
|
|
- Device ownership is delegated to `renderer_device`: wrappers here strongly retain `Device` through its `DeviceObject` or explicit fields. Do not place a device-owning wrapper back inside `DeviceInner`.
|
|
- `Image::from_swapchain_raw(image, device, format, extent, usage)` wraps a swapchain-owned raw image without taking image ownership. `Swapchain::acquire_image` passes the swapchain device, configured format/usage, and configured extent with depth 1. Its descriptor remains TYPE_2D, one mip/layer, TYPE_1 samples, OPTIMAL tiling, PRESENT queue flags, UNDEFINED layout, and GpuOnly memory. Do not destroy that image as an independently allocated image.
|
|
- `ImageView` retains an `Arc<Image>` so its source image outlives the Vulkan view. `Image::create_view` in `renderer_resources` returns `ManuallyDrop<ImageView>`; renderer call sites transfer it deliberately (the swapchain path uses `ManuallyDrop::into_inner`).
|
|
- For allocated images, `Allocation::Owned` frees VMA allocation memory with its `DeviceObject`; `Allocation::Shared` permits shared ownership and only exposes mutable access when uniquely held; `Unmanaged` carries no allocation.
|
|
- `Swapchain` owns raw swapchain images and manually created acquire/release semaphores. Its drop path waits idle and destroys those semaphores; the `DeviceObject` destroys the swapchain handle.
|
|
- `SwapchainImage` / `SwapchainFrame` are `#[must_use]` acquired-frame values. Presentation consumes the frame; preserve its `Arc<Swapchain>` so image ownership stays valid until presentation.
|
|
- `Surface` uses a `DropGuard` for surface destruction and keeps its `Instance`; field order drops the swapchain lock first, then the guard, then the instance.
|
|
- `SingleUseCommand` and `CommandBuffer` use `DeviceObject<vk::CommandBuffer>`, whose device-layer destroy implementation is intentionally a no-op. Their own drops free command buffers through their pools. `SingleUseCommandPool` owns a raw mutex-protected pool and destroys it itself.
|
|
- `CommandList::submit` requires all commands to come from the same queue; `CommandBuffer::submit` checks queue-family compatibility.
|
|
- `CommandPool` is explicitly `!Sync`; `CommandBuffer` is explicitly `!Send` and `!Sync`. `SingleUseCommand` is explicitly `!Sync`. Preserve these thread-affinity constraints when changing allocation/submission wrappers.
|
|
- `CommandPool::alloc` currently calls `free_command_buffers()` before allocating, so its free-list is not an enduring reusable buffer cache. `CommandBufferFuture` frees the submitted buffer when the future is dropped.
|
|
- `SingleUseCommand` begins recording with `ONE_TIME_SUBMIT`; callers must end/submit according to its state and must not access it concurrently (it is `!Sync`).
|
|
- `CommandPools` is a thread-local registry sharded by thread and queue family; its unsafe `Send` implementation documents an assumption that shards are unused during drop/movement.
|
|
- `Surface::new_from_raw_window_handle` is unsafe: the caller keeps display/window handles valid while surface use continues.
|
|
- `Swapchain::release_resources` is an unsafe once-only pre-destruction operation; `Drop` invokes it and currently ignores the result of its internal idle wait.
|
|
- `Swapchain` uses separate synchronization semaphore lifetimes: acquire semaphores are sized for frames in flight, release semaphores are per swapchain image. Preserve their indexing, explicit destruction, and uniqueness; they are manually managed, not entries in `DevicePools`.
|
|
- Resource implementation details are owned by `renderer_resources`; `crate::buffers`, `crate::images`, and `crate::pipeline` resolve through crate-private aliases. Do not expose these aliases as renderer public modules.
|
|
- `Allocation::Shared` only permits `allocation_mut()` through `Arc::get_mut`; callers must not assume shared allocation contents can always be mutated.
|
|
- `ImageDesc` and `BufferDesc` omit `alloc_scheme` from both `Hash` and `Eq`; `Hash` omits `name`, and `Eq` also omits `name`. Check descriptor-cache assumptions before relying on either as a complete semantic identity.
|
|
- `Image::new_with_allocation` in `renderer_resources` validates memory size/type but still creates the raw image before validation; failure paths should account for cleaning up handles if this function is revised.
|
|
- `Surface::configure` replaces the current `Arc<Swapchain>` after creating the new one; in-flight tokens retain old swapchains, so preserve that overlap behavior on reconfiguration.
|
|
- `Swapchain::acquire_image` moves the potentially blocking acquire call to `smol::unblock`, waits on a pooled fence, and constructs the acquired view before returning the frame.
|
|
- `Renderer2::draw_graph` acquires a frame, imports its image, resolves/submits the graph with acquire/release semaphores, awaits completion, and then presents.
|
|
- The active public graph API is `render_graph/legacy.rs`. Other graph siblings are private experimental/incomplete implementation, not a replacement API; avoid relying on TODO scaffolding.
|
|
- `render_graph/legacy.rs` has partial/unfinished behavior in some paths; inspect the exact pass/resource path being changed.
|
|
- `GraphResource::simple_hash` is used as an import deduplication key, not a collision-proof identity or equality check. `resolve` appends an output-read pass.
|
|
- `RenderGraph::resolve` creates a `SingleUseCommandPool` on the device main queue and records pass callbacks into command buffers. The graph's task scheduling remains single-queue in this path.
|
|
- Some legacy graph commands (`clear_pass`, `present_pass`) are convenience helpers; `present_pass` is deprecated in favor of marking outputs with `Access::present()`.
|
|
- The newer `render_graph/commands.rs` and `recorder.rs` use additional `ResourceId`/typed resource wrappers, distinct from legacy `GraphResourceId` and `GraphResource`.
|
|
- The crate root also exposes `Extent2D` and `utils` alongside renderer modules and device re-exports.
|
|
- `SamplerDesc` implements float-aware equality and hashing with utility helpers; keep those implementations consistent if adding sampler fields.
|
|
- `Pipeline::new_graphics` supports dynamic rendering state through `RenderingState`; its render-pass and rendering choices are supplied in `GraphicsPipelineDesc`.
|
|
- `Buffer` and `Image` creation in `renderer_resources` allocates/binds through `DeviceInner::allocate_for_buffer` / `allocate_for_image` and `bind_buffer_memory` / `bind_image_memory`; device ownership and final VMA allocation release are handled by the device-layer wrappers.
|
|
- Resource types live in `renderer_resources`; the `renderer` aliases are crate-private and are not externally reachable as `renderer::buffers::...` etc.
|
|
- `Renderer2::new` selects an adapter with `None` for the surface, then creates the logical device using its retained display handle. `choose_adapter_default` does not evaluate raw surface compatibility.
|
|
- `render_graph/legacy.rs` implements the active public graph resolver. Its resources are ordered by `GraphResourceId`, and passes declare reads/writes through `PassDesc` so the resolver can derive dependencies and transitions.
|
|
|
|
## Conventions
|
|
- Nightly crate features in `lib.rs`: `negative_impls`, `map_try_insert`, `debug_closure_helpers`.
|
|
- Use `renderer`'s re-exported `Device`, `DeviceOwned`, and `DeviceRef` APIs rather than adding a parallel device ownership model.
|
|
- Do not commit changes. Verify with `cargo check --workspace`; device-layer tests run with `cargo test -p renderer_device`.
|
|
- Run focused crate checks with `cargo check -p renderer`; use `cargo check --workspace` for changes crossing crate boundaries.
|
|
- Resource APIs live in the public `renderer_resources` crate. Renderer aliases are deliberately crate-private; keep the renderer-facing API intentional when adding re-exports.
|
|
|
|
## Change guidance
|
|
- Add buffer/image/pipeline resource constructors in `renderer_resources`, using its `renderer_device` re-exports and retaining the correct device/handle owner. Do not add a dependency from `renderer_resources` back to `renderer`.
|
|
- For every resource, identify whether it owns, shares, or merely borrows Vulkan handles and memory before implementing `Drop` or changing field order.
|
|
- Keep image-view format, aspect, mip, layer, and view-kind validation in `renderer_resources`; Vulkan view compatibility depends on the backing image description and create flags.
|
|
- A swapchain image is not an independently destroyable `Image`; retain its swapchain and do not attach a GPU allocation to it.
|
|
- Keep allocation freeing and Vulkan handle destruction in `renderer_resources` in a safe drop order: image/buffer before bound allocation memory.
|
|
- Use the correct queue family set in buffer/image descriptions when resources are accessed from more than one queue; `QueueFlags` here is the device-layer selection type.
|
|
- Command buffer recording and queue submission have separate synchronization requirements. Preserve queue locks and ensure an in-flight command buffer/pool is not freed before completion.
|
|
- `CommandBufferFuture` owns the submitted command buffer until the future is dropped; callers that discard it early change its lifetime and must ensure GPU work is complete.
|
|
- Surface/window raw-handle validity is the caller's obligation; retain the originating instance until surface teardown.
|
|
- `SwapchainImage` and `SwapchainFrame` are must-use ownership tokens; keep presentation consuming the token to prevent duplicate presentation.
|
|
- Graph callbacks in `PassDesc` capture resources across graph resolution. Preserve their lifetimes and correctly declare resource reads/writes for dependency/barrier generation.
|
|
- When adding a public graph API, check whether it belongs to the legacy `render_graph` re-export or an unexposed experimental sibling; do not accidentally promise incomplete internals.
|
|
- The newer typed graph modules have stubs and `todo!()` paths; add tests and complete the actual resolve/record flow before routing runtime rendering through them.
|
|
- Egui texture IDs map separately between `egui::TextureId` and internal `TextureId`; updates and frees must maintain both maps and texture manager state.
|
|
- Shader paths are currently relative to the workspace working directory; preserve or intentionally redesign that runtime asset lookup contract.
|
|
- `renderer_resources::util` owns the float helpers and `F32`; renderer re-exports those names through `renderer::util`. Keep `SamplerDesc` float equality/hash helpers in sync for every field so `SamplerCache` map keys remain valid.
|
|
- For pipeline changes, account for both legacy render-pass and dynamic-rendering descriptions and the shared `device.pools.pipeline_cache.raw` handle.
|
|
- `util::DropGuard` invokes its closure at drop; do not allow a closure to outlive raw Vulkan dependencies it captures.
|
|
- The `egui` pre-pass currently packs texture updates into a staging image/buffer and registers graph work; texture-free deltas and upload ordering deserve explicit coverage when changing the integration.
|
|
- The private font-atlas image is CPU memory, not a Vulkan `images::Image`; do not conflate the similarly named `Image` types when editing `text.rs`.
|
|
- `weak_vec` moved to `renderer_resources::util` and is not re-exported by `renderer`; it prunes dead weak references on capacity pressure. Image-side view tracking should remain weak to avoid ownership cycles.
|
|
- Render graph IDs are distinct: legacy `GraphResourceId` wraps `u32`; the newer internal `resources::ResourceId` also wraps `u32` (constructed via `ResourceId::from_u32`).
|
|
- Public `commands::traits::CommandBufferExt` contains raw recording convenience methods; those methods assume valid command-buffer state and do not all enforce it at runtime.
|
|
- `SwapchainConfiguration.image_count` is treated as the additional desired in-flight count during validation, while creation asks for minimum surface image count plus that configured value. The fallback-configuration helper assigns a total count.
|
|
- The supported surface capability extension query can filter present modes based on the selected mode; its behavior depends on optional `InstanceExtensions` support.
|
|
- `rendering::Wireframe` allocates GPU buffers and submits a one-shot upload before returning, then records its rendering pass through the legacy graph.
|