vidya/docs/renderer.md
2026-10-22 22:47:15 +02:00

28 KiB

Renderer and repository guide

Repository overview

Vidya is a Rust 2024 workspace containing a low-level Vulkan renderer, a Bevy-facing integration crate, small game/window crates, and a separate Bevy text crate. The root package is named vidya; the current runnable entry point is the bevy_integration example. The renderer is primarily an ash-based Vulkan layer, while bevy_vulkan_render sets up Bevy render-world infrastructure and currently contains a mock render system rather than a finished Bevy rendering backend.

Line counts below are wc -l counts over Rust source files in each package (not generated/dependency code); for the renderer the 22 files total 14,100 lines. This source inventory differs from the supplied starting estimate of about 24,600 lines across 23 files: the current crates/renderer/src glob has 22 .rs files and their measured total is 14,100 lines.

Workspace member Approx. Rust LOC Purpose
crates/renderer 14,100 Vulkan instance/device/resources, command recording, swapchains, render graph, egui and glyph-atlas code.
crates/bevy_vulkan_render 1,518 Bevy render sub-app, world synchronization/extraction, threaded scheduling, and window surfaces.
crates/game 315 Standalone winit/egui demo using renderer::Renderer2.
crates/text 484 Bevy asset/ECS-oriented text and font-atlas implementation built on cosmic-text.
crates/window 1 Minimal winit dependency crate; its src/lib.rs is empty.
root vidya package 45 Example host and dependency on bevy_vulkan_render; three lines of src/lib.rs plus the 42-line example.

The code has two overlapping integration paths: crates/game directly creates the renderer and surfaces; the root example constructs a Bevy App with Bevy's standard plugins plus bevy_vulkan_render::RenderPlugin.

Dependencies and toolchain

Area Repository configuration
Rust rust-toolchain pins nightly; packages use edition 2024.
Local Bevy Workspace dependencies point to /var/code/bevy-engine/bevy and its crate directories, not crates.io: bevy, bevy_app, bevy_asset, bevy_derive, bevy_ecs, bevy_hierarchy, bevy_log, bevy_math, bevy_platform, bevy_reflect, bevy_tasks, bevy_time, bevy_transform, bevy_utils, bevy_window, and bevy_winit. This checkout is Bevy 0.20.0-dev as recorded in the project setup. bevy_ecs enables multi_threaded and trace.
Vulkan/window ash 0.38.0, ash-formats for Vulkan format metadata/helpers, including the adopted FormatHelpers sizing and clear-value helpers, ash-window 0.13.0, vk-mem 0.5.0, gpu-allocator from the janis-bhm/gpu-allocator GitHub main branch, winit 0.30.5, and raw-window-handle 0.6. Renderer allocation code specifically uses gpu-allocator; vk-mem is declared but is not the allocation path in device.rs, buffers.rs, or images.rs.
Shared crates glam 0.33 with bytemuck, rand 0.10, derive_more 2, petgraph 0.8, egui 0.32, tracing, parking_lot, among others. See root [workspace.dependencies] for the complete list.
Text cosmic-text 0.12.1, guillotiere 0.6.2, and sys-locale 0.3.2 are used in the renderer and/or text crate.

crates/renderer/src/lib.rs enables these nightly features (the text crate separately opts in to debug_closure_helpers):

Feature gate Use visible in the code
negative_impls Explicitly marks command pools/buffers and single-use command objects as !Send/!Sync where Vulkan command-pool thread ownership is constrained (commands.rs).
map_try_insert Uses BTreeMap::try_insert/HashMap::try_insert in graph and resource bookkeeping.
core_io Imports core::io::BorrowedBuf in the graph-builder egui demo.
core_io_borrowed_buf Enables the borrowed-buffer fill APIs used alongside BorrowedBuf in that demo.
slice_partition_dedup Deduplicates selected queue family indices in-place (queue.rs).

Renderer crate

Public surface and code map

renderer/src/lib.rs owns core device/feature types and exports ash, instance, device, queue, commands, render_graph, rendering, swapchain, sync, and util. buffers, images, pipeline, text, debug, and the egui implementation are declared as private modules. egui.rs is loaded as the private egui_pass module; rendering re-exports its two egui pass functions. The render_graph submodules are also private; render_graph/mod.rs currently re-exports the legacy graph API only.

Source Responsibilities and key types Relationship / visibility
lib.rs Crate root, feature/property representations (PhysicalDeviceFeatures, PhysicalDeviceProperties, PhysicalDeviceInfo), Error/Result, SamplerCache, EguiState, Renderer2, and the internal texture manager. Renderer2::new, create_surface, and async draw_graph are the higher-level convenience flow. Public exports and renderer assembly; creates the instance, adapter, device, graph, and submission/presentation flow.
instance.rs Vulkan loader/instance setup (Instance, InstanceDesc, DebugUtilsCreateInfo), layer and extension selection, physical-device enumeration, adapter ranking via choose_adapter_default, and surface capability queries. Instance is re-exported from crate root and hands PhysicalDeviceInfo to device.rs.
device.rs Logical Device/DeviceInner, required extensions and feature chain, GPU allocator, device object lifetime helpers (DeviceOwned, DeviceObject family), resource allocation types (Allocation, AllocationStrategy), and reusable device pools. Adapter creation calls into queue.rs, sets up queues and gpu_allocator, and is held by buffers, images, commands, and sync objects.
queue.rs Queue, QueueFamily, DeviceQueues, DeviceQueueInfos, queue capability flags and family selection. Called by logical device initialization; supplies graphics/main, compute and transfer queues to the device.
buffers.rs BufferDesc and Buffer: Vulkan buffer creation, queue-sharing mode, memory allocation/binding, optional mapped map/map_mut, raw handle and length. Private module; used by commands, renderer helpers, graph implementations and egui uploads.
images.rs ImageDesc, Image, ImageViewDesc, ImageView, Extent, Offset, MipRange, QueueOwnership. Uses ash-formats format metadata. Handles owned and swapchain images, memory, views, extents and subresources. Private module; swapchains wrap their external images here. Used by render graph and egui.
memory.rs Empty placeholder (one blank line). Not an allocation implementation; allocation is in device.rs and resource constructors.
pipeline.rs Descriptor/pipeline descriptions and objects: DescriptorPool, DescriptorSetLayout, PipelineLayout, Sampler, ShaderModule, Pipeline, PipelineCache; supports compute and graphics pipeline construction. ShaderModule::new_from_path reads SPIR-V words and new_from_memory accepts words directly. Private module; commands.rs binds pipelines/descriptors; EguiState and rendering helpers build concrete pipelines. No shader compiler/build pipeline is defined here.
commands.rs Low-level command-pool and command-buffer abstractions (SingleUseCommandPool, CommandList, command-buffer types/state, CommandBufferExt), recording/copy/barrier operations and queue submission/futures. The legacy graph and one-off uploads use these command buffers; synchronization objects come from sync.rs.
swapchain.rs Surface, Swapchain, SwapchainConfiguration, SwapchainImage, SwapchainFrame; creates/configures surfaces and swapchains, acquires images, and presents them. Surface owns a replaceable optional swapchain. Swapchain images are represented by images::Image/ImageView; uses Fence and semaphores.
sync.rs Fence, binary/timeline Semaphore, SyncThreadpool, FenceFuture, and pooled synchronization objects. The swapchain and command submission code use fences/semaphores; DeviceInner owns a sync waiter pool and device pools.
render_graph/mod.rs Declares legacy, commands, graph_builder, recorder, and resources; re-exports legacy::*. Despite the internal new graph files, the externally reachable RenderGraph is the legacy.rs implementation. #![allow(dead_code)] covers the module tree.
render_graph/legacy.rs Current reachable graph API: RenderGraph, GraphResourceId, GraphResourceDesc, GraphResource, PassDesc, RenderContext, Access, load/store ops and helpers such as clear_pass/present_pass. Stores imported/created resources, pass access declarations and output resources; resolve orders passes and records a CommandList for its caller to submit. Re-exported by render_graph/mod.rs; used by Renderer2::draw_graph, rendering/mod.rs and egui.rs. Although named legacy, this is the path called by the current rendering code.
render_graph/resources.rs Newer command-graph resource abstraction: Resource, ResourceId, ranges/access records, Buffer/Texture handles and slices/regions, Read/Write/ReadWrite wrappers. Internal to newer commands.rs/recorder.rs; not re-exported from render_graph/mod.rs.
render_graph/commands.rs New command API (Command, ImportResource, Copy, ClearTexture, UpdateBuffer, rendering/pipeline/buffer/descriptor/draw commands, PipelineBarrier); each command declares resource side effects and implements recording. Uses newer resource wrappers and CommandRecorder, separate from the currently re-exported legacy PassDesc graph. Several commands remain incomplete; see current state.
render_graph/recorder.rs New CommandRecorder, CommandList, RenderPass, side-effect maps, command metadata and access/barrier collection. Intended to translate new graph commands to Vulkan command buffers; recorder resource lookups and barrier application contain explicit unimplemented code.
render_graph/graph_builder.rs New GraphResourceRegistry and GraphBuilder, wrappers for buffer and staging resources, plus an api_demo::egui_graph sample that packs vertices/indices/draw data with BorrowedBuf. Not a wired-up implementation of the current legacy graph: the resource-creation methods are todo!() and part of the sample is unimplemented!().
rendering/mod.rs Wireframe GPU data/pipeline example and Wireframe::pass; exports egui_pre_pass and egui_pass from the egui module. Builds passes on the legacy RenderGraph; uses Buffer, Pipeline, graph access declarations, and commands.
egui.rs egui_pre_pass uploads egui texture deltas (including staging/atlas packing) and egui_pass adds drawing work; uses EguiState, texture IDs, buffers, images, samplers, pipeline, and graph commands. Private module imported by rendering/mod.rs; current code exercises the legacy graph, not the in-progress typed graph API.
text.rs Private glyph rasterization and atlas data structures (FontId, font stores/atlas sets, get_outlined_glyph_texture) based on cosmic-text and guillotiere. Embeds assets/fonts/Roboto.ttf and NotoSansSC.ttf. Renderer-local/private text implementation; not the same module as the public Bevy-oriented crates/text crate. Contains an unfinished subpixel-mask case.
util.rs Shared Vulkan helpers and data types (DebugName, Rect2D, F32, Rgba, Rgba8, bit iterators, CStringList, drop/lifetime helpers and internal WeakVec). Used across resource, debug, image and graph code.
debug.rs Vulkan debug messenger callback, translating validation severities into tracing levels. Used by instance setup; intentionally private.

Initialization, resources, and commands

The normal setup path begins with Instance::new(&InstanceDesc) (instance.rs), which loads Vulkan through ash, enables supported requested/WSI extensions and creates debug-utils state. The default description requests surface support and the Khronos validation layer/settings (unsupported layers are logged and filtered). Adapter enumeration exposes device properties/features as PhysicalDeviceInfo.

Instance::choose_adapter_default(surface, required_extensions, required_features) ranks adapters. Its comparator attempts to prefer swapchain/required-extension support and requested PhysicalDeviceFeatures, requires preference for Vulkan 1.3 synchronization2, dynamic_rendering, and maintenance4, then prefers discrete GPUs, newer API versions, more device-local memory, and larger 2D image limits. It is ranking logic, not a robust suitability filter: required extension checks return Less for the left-hand candidate rather than first excluding every unsuitable device, and create_logical_device warns and omits unsupported requested extensions. An important limitation visible in code: the surface argument is currently ignored at the // TODO surface-compatibility check; choosing an adapter does not actually validate that it can present to that surface. PhysicalDeviceFeatures is a chain of core and version/extension feature structs; create_logical_device pushes those into vk::DeviceCreateInfo.

PhysicalDeviceInfo::create_logical_device asks DeviceQueueInfos::select_queue_families for queue families, creates the logical device, retrieves queues, creates extension loaders, then creates gpu_allocator::vulkan::Allocator and resource pools. Queue selection chooses the first family containing both GRAPHICS and COMPUTE, decrements its available queue count, and asserts that this selected family has PRESENT. It then takes another available compute family and transfer family if found; missing compute/transfer queues alias the graphics queue. Therefore a display handle that did not produce presentation support can reach the explicit Selected graphics queue family does not support presentation assertion. The presentation flags are derived from a display handle, and the queue-selection path is not a general strategy for selecting a distinct present-only family.

The allocator is stored behind a mutex on DeviceInner. Both Buffer::new and Image::new create the Vulkan object, query memory requirements, allocate using AllocationStrategy::{AllocatorManaged,Dedicated}, bind the returned allocation, and retain it for destruction. BufferDesc/ImageDesc include memory location, usage, queue family flags and allocation strategy. Buffers may expose mapped bytes when the allocator returns a mapped allocation. Images also support externally owned swapchain images and cached weak image-view references. memory.rs itself has no implementation.

There is no universal upload subsystem in buffers.rs/images.rs; upload paths are explicit at call sites. For example, Wireframe::from_triangles_indexed writes positions/indices/colors to a CpuToGpu staging buffer, copies into GpuOnly vertex/index buffers via a one-off command buffer, and adds transfer-to-vertex/index barriers. The egui pre-pass similarly writes texture data to mapped staging storage and records graph copy work. Shader modules load SPIR-V from a path or word slice; pipeline creation consumes GraphicsPipelineDesc or ComputePipelineDesc, descriptor layouts/pools, and optional PipelineCache. Shader source compilation is outside this crate's code.

Surfaces, frames, and synchronization

Surface::new_from_raw_window_handle creates a Vulkan surface from raw handles; Surface::headless is also defined. Surface::configure avoids recreation for an equal config on the same device, otherwise creates a replacement swapchain using the old swapchain handle. SwapchainConfiguration includes extent/format/present mode/usage and image-count options, while capability validation chooses compatible settings. Acquiring a frame asynchronously reserves a per-frame acquire semaphore, performs acquire_next_image through smol::unblock under the swapchain lock, creates an Image and view for the acquired swapchain image, and waits on a fence. SwapchainImage and SwapchainFrame are must_use and expose present.

Swapchain synchronization is not just a generic frame fence ring: the implementation creates acquire semaphores based on the number of in-flight frames and a release semaphore for every swapchain image. Submission waits on the acquire semaphore and signals the release semaphore; presentation waits on the release semaphore. Swapchain::max_in_flight_images is image count minus the surface minimum image count. The general sync.rs API also supports dedicated/pooled fences, binary/timeline semaphores, and fence futures serviced by a bounded waiter thread pool. Renderer2::draw_graph demonstrates acquire → import framebuffer → graph resolve → submit using acquire/release semaphores → await → present.

Render graph: current path and in-progress path

The currently wired graph is render_graph::legacy::RenderGraph, re-exported by render_graph/mod.rs. It owns resources and PassDescs with declared read/write Access values and optional one-shot RecordFns. Imported framebuffer/images/buffers and created resource descriptions receive graph IDs. Outputs are marked explicitly; resolution determines which passes feed outputs and prepares resources/barriers before executing recorded passes. Renderer2::draw_graph constructs this graph and calls resolve; the wireframe and egui rendering functions add passes to it. In legacy.rs, comments discuss future resource aliasing, but should not be read as proof that aliasing is fully implemented.

The sibling resources.rs, commands.rs, recorder.rs, and graph_builder.rs form a newer typed command/resource design, but are private submodules and are not re-exported or connected to the root RenderGraph API. resources.rs tracks buffer/image ranges and stage/access/layout side effects; commands.rs describes copies, clears, updates, rendering scopes, binding, draws and barriers; recorder.rs is intended to lower command lists and insert transitions; graph_builder.rs contains the graph-resource registry and a sample upload/build path. That path uses staging buffers and core::io::BorrowedBuf for writing several egui draw-data arrays into one mapped region. However, the builder methods that create command lists and make/import buffers are todo!(), and the recorder still has unimplemented handle accessors/barrier code. Treat this as an experimental/incomplete path, not as the graph used by draw_graph.

Text, egui, and orchestration

The renderer's private text.rs embeds two font files and manages per-font/per-size glyph atlases; get_outlined_glyph_texture rasterizes glyphs through cosmic-text/Swash and packs masks/color glyphs. It is separate from crates/text/src/lib.rs, which defines a Bevy FontData asset, font loader, ECS text component and its own atlas machinery. Both source trees embed font paths from assets/fonts.

The renderer egui path in egui.rs consumes egui::FullOutput: texture deltas are converted to image/atlas staging uploads, and tessellated primitives are emitted as rendering passes using the egui pipeline and descriptor state. rendering/mod.rs re-exports egui_pre_pass and egui_pass alongside the Wireframe graph pass. Renderer2 is a direct-use convenience owner for instance/device/samplers/textures/egui state and surfaces; its draw_graph orchestrates one frame. Bevy has a separate plugin/schedule integration and currently does not call this orchestration as an active render backend.

Bevy integration crate

crates/bevy_vulkan_render/src/lib.rs declares RenderPlugin, the RenderApp sub-app label, Render and RenderStartup schedules, and ordered RenderSystems sets (ExtractCommands, Prepare, Render, Cleanup, PostCleanup). RenderPlugin::build adds ExtractPlugin, initializes render schedules on the sub-app and installs mock_render_system. RenderDevice is a Bevy resource wrapper around renderer::device::Device. An init_vulkan function exists and requests sampler anisotropy, non-solid fill, multi-draw-indirect, Vulkan 1.1/1.2/1.3 features and mesh shaders, but it is unused according to both source and compiler dead-code warnings.

extract_plugin.rs creates the RenderApp SubApp, installs ExtractSchedule, and arranges sync/extract each frame. It moves the main World into a temporary MainWorld resource for extraction, restores it afterward, and delays ExtractSchedule deferred-command application into the render schedule's ExtractCommands set. This leaves the main thread free earlier for pipelining.

extract_param.rs defines Extract<'w, 's, P>, a SystemParam wrapper for applying a read-only system parameter to MainWorld during ExtractSchedule. It registers read-only main-world access and intentionally disallows contained mutating system params.

sync_world.rs implements SyncWorldPlugin and the SyncToRenderWorld marker. Observer records are processed by entity_sync_system to maintain the main/render entity link: RenderEntity on the main side points to the render entity; MainEntity points back. Custom QueryData implementations let Query<RenderEntity> and Query<MainEntity> return the wrapped Entity directly. Temporary render entities are despawned in post-cleanup. A TODO calls out batching pending sync records.

window/mod.rs holds extracted physical window metadata in ExtractedWindows, keyed by entity, and stores a Surfaces map keyed per window entity. extract_windows tracks primary window, dimensions, closing/removal, and initial presentation state. need_surface_configuration detects new/unconfigured or resized windows; configure_surfaces creates a Surface from the raw handles and configures/reconfigures its swapchain. These functions and resources are currently reported unused by the compiler, so their source structure is present but not yet integrated into active plugin systems.

rendering.rs defines RenderingPlugin and RenderExtractApp. When installed after RenderPlugin, it transfers the RenderApp between the main app and a spawned render thread using bounded async channels, executes render-app updates there, and extracts/sends it back each frame. The root example installs RenderPlugin, but does not install RenderingPlugin; the current RenderPlugin only installs a mock render function.

Current state and known unfinished work

Build and warnings

On the checked-out tree, cargo check --workspace --all-targets completes successfully. It emits warnings, including deprecated glam::Mat4::perspective_lh and Mat4::look_at_rh calls in rendering/mod.rs, a deprecated standard-library atomic fetch_update use, unused imports/variables, and dead code. Specifically, compiler warnings expose that init_vulkan, the window extraction/configuration functions, render_start/render_startup, and parts of sync tracking are unused. This is a compile check, not evidence that a Vulkan device/surface runs successfully on a target machine.

Marker inventory (source scan)

Across crates/renderer/src, crates/bevy_vulkan_render/src, crates/game/src, and crates/text/src, a literal scan finds 21 TODO/FIXME occurrences and 19 todo!/unimplemented! occurrences. The latter includes test-only code; it does not imply 19 active runtime paths. The locations and notable behaviors are:

File / lines Finding
renderer/src/instance.rs:368 Surface compatibility in default adapter ranking is a TODO; :474 XCB presentation support is todo!("xcb").
renderer/src/queue.rs:326-329 Explicit assertion requires selected graphics+compute family to present.
renderer/src/render_graph/graph_builder.rs:41,44,53,59 New builder methods are todo!(); :266 has an unimplemented!() factory in the demo path.
renderer/src/render_graph/recorder.rs:23,27,31,35,39,43,344 CommandRecorder accessors and image barrier recording are unimplemented/todo. :339 also notes missing image aspect information.
renderer/src/render_graph/commands.rs:316,592 Render load/store operations are noted as TODO; PipelineBarrier::apply is todo!().
renderer/src/render_graph/resources.rs:46,250 TODOs flag buffer-row range units and possible write-order conflict handling.
renderer/src/render_graph/legacy.rs:799,809 TODOs in graph hazard/dependency handling for first-pass checks.
renderer/src/text.rs:71,162,281-282 Atlas naming, host-visible/coherent image considerations, and SubpixelMask glyph content remain TODO/todo.
renderer/src/sync.rs:361,373 vk::Handle::from_raw for binary and timeline semaphore wrappers is unimplemented.
renderer/src/device.rs:1107-1113 A #[cfg(test)] helper contains a summon stub with unimplemented!(); test-only.
renderer/src/lib.rs:610; swapchain.rs:593; commands.rs:749; egui.rs:320 Additional behavior/design TODO comments; swapchain present-id/wait support is noted as optional work.
bevy_vulkan_render/src/sync_world.rs:217 TODO to batch pending sync records.
game/src/main.rs:49,58,85 TODOs call out error handling, resize handling and unfinished draw-request logging/path.
text/src/lib.rs:123,214,336-337 Separate Bevy text implementation has similar atlas/image TODOs and an unfinished subpixel-mask case.

The largest architectural caveat is that the typed render_graph command/recorder/builder files are not active wiring: the legacy graph is still the sole graph re-export, and the new recorder contains explicit stubs. The Bevy crate likewise has substantial extraction/window/thread infrastructure, but its current installed render schedule uses a mock system and its Vulkan initializer/surface functions are unused. Bevy-facing rendering should therefore be described as scaffolding/integration work in progress rather than a complete rendered-frame backend.

Build and run

  1. Use the repository's nightly toolchain (rustup will honor rust-toolchain). The renderer has compile-time include_bytes! references to font files; the repository currently contains the generated flat and directory layouts. To regenerate the expected layouts, run the fetch script from the font directory:

    cd assets/fonts
    ./fetch.sh
    cd ../..
    

    fetch.sh downloads the upstream Google Fonts files; network access is needed only for this prerequisite. It creates Roboto/Roboto-VariableFont_wdth,wght.ttf and Noto_Sans_SC/NotoSansSC-VariableFont_wght.ttf from flat files. Renderer-local text also includes flat Roboto.ttf and NotoSansSC.ttf.

  2. Check the workspace and all targets:

    cargo check --workspace --all-targets
    
  3. Run the Bevy example:

    cargo run --example bevy_integration
    

    The example creates a Bevy app with minimal, asset, window/winit, camera/mesh/text and VulkanRenderPlugin plugins. It currently prints Hello, world! before running; the installed Vulkan render plugin uses a mock render system. For direct renderer/egui experimentation, crates/game/src/main.rs is the standalone winit entry point, though it too has TODO-marked draw/resize work and requires a Vulkan-capable system/display.