Skip to main content

concinnity_core/render/
backend.rs

1//! RenderBackend trait: the union of methods every graphics backend
2//! implements, dispatched dynamically by GraphicsSystem so the per-frame
3//! step + setup logic lives in one cfg-free copy instead of three.
4//!
5//! Each concrete backend (MtlContext / DxContext / VkContext) supplies a
6//! thin forwarder impl that delegates to the existing inherent methods
7//! (see metal/backend.rs, directx/backend.rs, vulkan/backend.rs).
8//!
9//! Two cross-backend signature variances are handled here:
10//!   - `upload_skinned`: Metal uses three shader payloads (vert + frag +
11//!     shadow); DX/VK use one (frag). The trait method takes all three;
12//!     DX/VK ignore the unused bytes.
13//!   - `setup_chunk_streaming`: Metal binds chunk textures per draw and
14//!     ignores the (texture_slot, normal_map_slot) args; DX/VK bake them
15//!     into a shared descriptor at setup time.
16//!
17//! `render_stats` has a default no-op impl so a backend with no draw-call /
18//! object counters need not override it; all three shipping backends do.
19
20use crate::components::ShaderPrograms;
21use crate::gfx::auto_exposure::AutoExposureSettings;
22use crate::gfx::mesh_payload::{SkinnedVertex, Vertex};
23use crate::gfx::profile::RenderStats;
24use crate::gfx::render_types::{
25    LineVertex, MaterialUniforms, PostProcessTunables, SkinnedDrawObject, TextDrawCall,
26};
27use crate::gfx::rt_reflections::RtReflectionSettings;
28use crate::gfx::ssao::SsaoSettings;
29use crate::gfx::ssgi::SsgiSettings;
30use crate::gfx::ssr::SsrSettings;
31use crate::render::backend_init::{BackendInit, SwapchainConfig, WorldShader};
32use crate::render::error::{RenderError, RenderResult};
33use crate::render::input::RenderInput;
34use crate::render::keymap::KeyMap;
35use crate::render::scene_flow::SceneControl;
36use crate::render::volumetric_fog::FogSettings;
37use alloc::string::String;
38use alloc::string::ToString;
39use alloc::vec::Vec;
40
41/// Per-frame inputs for [`RenderBackend::draw_frame`]. `world_hidden` is set when
42/// an opaque menu backdrop covers the scene: the backend skips every world pass
43/// and presents only the overlay (`text_calls`) over a cleared target.
44#[derive(Clone, Copy)]
45pub struct FrameParams<'a> {
46    /// Seconds since the world started, for time-driven effects.
47    pub elapsed: f32,
48    /// Vertical field of view in radians.
49    pub fov_y_radians: f32,
50    /// Near clip distance in world units.
51    pub near: f32,
52    /// Far clip distance in world units.
53    pub far: f32,
54    /// World-space camera position.
55    pub cam_pos: [f32; 3],
56    /// Overlay draw calls for this frame.
57    pub text_calls: &'a [TextDrawCall],
58    /// Expanded line ribbons (`lines::build_vertices`) for this frame's camera,
59    /// drawn depth-tested into the scene after the world passes. Empty on any
60    /// frame that submits no lines, which also drops the pass from the graph.
61    pub lines: &'a [LineVertex],
62    /// `true` when an opaque menu backdrop covers the scene.
63    pub world_hidden: bool,
64    /// Viewport view mode + show flags for the frame (`ViewOverrides` when the
65    /// editor publishes one, defaults otherwise). Backends run their seeded
66    /// graph inputs through `render_graph::apply_view` and steer the composite
67    /// by the mode.
68    pub view_mode: crate::gfx::view_modes::ViewMode,
69    /// Feature passes to run this frame.
70    pub show: crate::gfx::view_modes::ShowFlags,
71    /// Rows of the rotation taking a world-space direction into the environment
72    /// cubemaps' baked frame (`SkyOrientation::sample_rows`). The backend holds
73    /// it for the frame and uploads it into every uniform block whose pass
74    /// samples the sky, so the sky, the IBL and the reflections turn together.
75    /// Identity in a world with no `SkyRotation`.
76    pub sky_rot: [[f32; 4]; 3],
77}
78
79/// One streamed chunk's geometry plus placement, supplied to
80/// [`RenderBackend::add_chunk_mesh`]. `frame` reclaims retired deferred frees
81/// before the chunk is placed in the streaming headroom.
82#[derive(Clone, Copy)]
83pub struct ChunkMesh<'a> {
84    /// Chunk vertices.
85    pub verts: &'a [Vertex],
86    /// Chunk indices, mesh-relative.
87    pub idxs: &'a [u16],
88    /// Column-major placement matrix.
89    pub model: [[f32; 4]; 4],
90    /// Index into the shared texture pool for the albedo map.
91    pub texture_slot: usize,
92    /// Index into the shared texture pool for the normal map.
93    pub normal_map_slot: usize,
94    /// Per-chunk material scalars.
95    pub material: MaterialUniforms,
96    /// Current frame number, used to reclaim retired deferred frees.
97    pub frame: u64,
98}
99
100/// One draw slot's fresh geometry, supplied to
101/// [`RenderBackend::rebuild_static_geometry`] when an asset hot-reload
102/// changed its vertex / index count and the slot can no longer hold the new
103/// data in place. The backend rebuilds the entire shared vertex / index
104/// buffer; draws not named here keep their current geometry, copied byte-for-
105/// byte from the live buffers. `indices` are mesh-relative (0-based); the
106/// backend rebases them onto whatever new vertex region the draw lands in.
107pub struct DrawGeometryUpdate {
108    /// The draw slot whose geometry is replaced.
109    pub draw_idx: usize,
110    /// Replacement vertices.
111    pub vertices: Vec<Vertex>,
112    /// Replacement indices, mesh-relative.
113    pub indices: Vec<u16>,
114    /// One slice per additional LOD, ordered mip 0 → mip N-1. Each is
115    /// `(switch_distance, mesh-relative indices)`. Empty for meshes
116    /// declared `lod_levels <= 1`.
117    pub lod_alternates: Vec<(f32, Vec<u16>)>,
118}
119
120/// One skinned draw slot's fresh geometry, supplied to
121/// [`RenderBackend::rebuild_skinned_geometry`] when an asset hot-reload
122/// changed its vertex / index count and the slot can no longer hold the new
123/// data in its existing region of the shared skinned vertex / index buffers.
124/// The backend rebuilds both shared buffers; slots not named here keep their
125/// current geometry, copied byte-for-byte from the live buffers and re-based
126/// onto whatever new vertex region they land in. `indices` are mesh-relative
127/// (0-based); the backend rebases them onto the new vertex region.
128pub struct SkinnedDrawGeometryUpdate {
129    /// The skinned slot whose geometry is replaced.
130    pub skinned_index: usize,
131    /// Replacement vertices.
132    pub vertices: Vec<SkinnedVertex>,
133    /// Replacement indices, mesh-relative.
134    pub indices: Vec<u16>,
135}
136
137/// The post-rebuild layout for one skinned slot, returned by
138/// [`RenderBackend::rebuild_skinned_geometry`] so the asset hot-reload
139/// helper can refresh its `SkinnedMeshSourceEntry`s'
140/// `vertex_base` / `vertex_count` / `index_count` to point at the new
141/// regions. Returned for every slot (both the ones whose geometry was
142/// replaced and the ones whose geometry was carried over) because the
143/// rebuild may have shifted every slot's `vertex_base`.
144/// Constructed only by the `cn debug` binary's skinned-rebuild reload pass;
145/// reads as dead under `cargo check --lib`.
146pub struct SkinnedSlotLayout {
147    /// The skinned slot this layout describes.
148    pub skinned_index: usize,
149    /// First vertex of the slot's region in the shared skinned buffer.
150    pub vertex_base: u32,
151    /// Vertices in the slot's region.
152    pub vertex_count: usize,
153    /// Indices in the slot's region.
154    pub index_count: usize,
155}
156
157/// The resolved per-feature quality settings for [`RenderBackend::apply_quality_settings`].
158/// `GraphicsSystem` derives these from its stored `PostProcessConfig` (with the
159/// user's persisted toggle overrides applied) whenever a Quality-group toggle
160/// changes, so the backend receives ready-to-use settings rather than re-deriving
161/// from the asset. Each `Option` mirrors the init-time gate: `None` means the
162/// feature is off and its passes / resources should be torn down; `Some` means it
163/// is on and its resources should exist. A backend without a live-rebuild path
164/// ignores this (the choice still persists and applies at the next launch).
165pub struct QualitySettings {
166    /// Temporal anti-aliasing on/off (the `Taa` anti-aliasing mode). The backend
167    /// additionally suppresses TAA while temporal upscaling is active (the scaler
168    /// does its own accumulation). The other anti-aliasing modes are the composite
169    /// FXAA edge filter, which rides `PostProcessTunables.fxaa` (pushed via
170    /// `update_post_process`), not this pass-rebuild payload.
171    pub taa: bool,
172    /// Screen-space ambient occlusion, or `None` when off.
173    pub ssao: Option<SsaoSettings>,
174    /// Screen-space reflections, or `None` when off.
175    pub ssr: Option<SsrSettings>,
176    /// Hardware ray-traced reflections. The backend further gates this on GPU
177    /// ray-tracing support, falling back to leaving it off when unsupported.
178    pub rt_reflections: Option<RtReflectionSettings>,
179    /// Screen-space global illumination, or `None` when off.
180    pub ssgi: Option<SsgiSettings>,
181    /// Per-axis divisor for the roughness-aware reflection blur target (the
182    /// reduced-resolution first pass of the SSR / RT reflection composite),
183    /// resolved from `PostProcessConfig.reflection_blur_resolution`. Every backend
184    /// sizes its blur target at render / this on a live reflection rebuild.
185    pub reflection_blur_scale: u32,
186    /// Auto-exposure, or `None` when off.
187    pub auto_exposure: Option<AutoExposureSettings>,
188    /// The authored exposure bias (stops) auto-exposure applies on top of its
189    /// adapted value; carried so a live auto-exposure enable matches init.
190    pub auto_exposure_bias_ev: f32,
191}
192
193/// GPU/device capability flags, queried from the backend once it is built.
194/// Surfaced so the settings menu can gray out (and make inert) toggles the
195/// device cannot honor -- e.g. ray-traced reflections on a GPU without hardware
196/// ray tracing. Mirrors an RHI-style capability set: a handful of bools held in
197/// memory and re-queried each launch, never persisted, so it is always correct
198/// for the current device + driver.
199#[derive(Clone, Copy, Debug)]
200pub struct DeviceCapabilities {
201    /// Hardware ray tracing for the RT-reflections pass: DXR 1.1 on DirectX, the
202    /// ray-query device extensions on Vulkan (and not under XeSS), and
203    /// `MTLDevice::supportsRaytracing` on Metal.
204    pub ray_tracing: bool,
205    /// Whether the upscaler implementation is a choice (FSR3 / DLSS / XeSS)
206    /// rather than fixed. DirectX and Vulkan offer the selection; Metal always
207    /// upscales through MetalFX, so the row has nothing to pick.
208    pub selectable_upscaler: bool,
209    /// Whether a retired build-time draw slot may be recycled by a runtime
210    /// clone. Metal's per-frame RT topology refresh re-admits recycled
211    /// build-time slots; DirectX / Vulkan key their cull BVH + RT tables to
212    /// fixed build-time indices and cannot refit, so only the runtime-append
213    /// region recycles there. Read by the engine's draw-slot allocator.
214    pub reuses_build_slots: bool,
215    /// Whether a built draw slot's material and cull distance may be rewritten
216    /// in place ([`RenderBackend::set_draw_material`] /
217    /// [`RenderBackend::set_draw_cull_distance`]). Metal rebuilds its per-object
218    /// buffer from the draw list every frame, so a rewritten slot draws with the
219    /// new material next frame; DirectX / Vulkan bake per-object material state
220    /// at build time and would keep drawing the old one. Read by the editor's
221    /// live draw seam, which sends the edit to a world rebuild instead.
222    pub rewrites_draws: bool,
223}
224
225impl DeviceCapabilities {
226    /// Every capability present. The trait default, so a backend that does not
227    /// report capabilities never wrongly disables a toggle (it keeps the prior
228    /// behavior: the feature no-ops with a warning on an incapable device).
229    pub const ALL: Self = Self {
230        ray_tracing: true,
231        selectable_upscaler: true,
232        reuses_build_slots: true,
233        rewrites_draws: true,
234    };
235}
236
237impl Default for DeviceCapabilities {
238    fn default() -> Self {
239        Self::ALL
240    }
241}
242
243/// Coarse GPU vendor class, derived per backend from the adapter's reported
244/// vendor id (DirectX / Vulkan) or unified-memory / Apple-family signals (Metal).
245/// Used only to pick default quality and to gate vendor-specific options (e.g.
246/// which upscalers to offer); never persisted.
247#[derive(Clone, Copy, Debug, PartialEq, Eq)]
248pub enum GpuVendor {
249    /// Apple silicon.
250    Apple,
251    /// NVIDIA.
252    Nvidia,
253    /// AMD.
254    Amd,
255    /// Intel.
256    Intel,
257    /// A vendor the probe does not recognise.
258    Other,
259}
260
261/// Coarse performance class for default-quality selection, ordered low -> high so
262/// callers can compare with `>=`. Each backend maps its native signals (memory
263/// budget, discrete / integrated, Apple GPU family) onto this via `classify_tier`.
264#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
265pub enum GpuTier {
266    /// Unknown hardware: the conservative default, never the top preset. Sorts
267    /// lowest so a comparison-based resolver treats it as the floor.
268    Unknown,
269    /// Integrated / low-power GPU: the lowest quality tier.
270    Integrated,
271    /// Older or small discrete GPU, or an Apple base M-series: entry quality.
272    EntryDiscrete,
273    /// Mainstream discrete GPU, or an Apple Pro: mid quality.
274    MidDiscrete,
275    /// Enthusiast discrete GPU, or an Apple Max / Ultra: high quality.
276    HighDiscrete,
277}
278
279/// A coarse, Copy snapshot of the active GPU's class, queried from the backend
280/// once it is built (mirrors `DeviceCapabilities`). Read at init to choose
281/// sensible default graphics quality; never persisted, re-queried each launch so
282/// it is always correct for the current device + driver. The GPU *name* is
283/// deliberately omitted (it is not `Copy`); a backend exposes the name separately
284/// when a UI needs it.
285#[derive(Clone, Copy, Debug)]
286pub struct GpuProfile {
287    /// The GPU's vendor.
288    pub vendor: GpuVendor,
289    /// The performance tier the probe placed the GPU in.
290    pub tier: GpuTier,
291    /// Dedicated VRAM on a discrete GPU, or the recommended working-set on a
292    /// unified-memory GPU. 0 when the backend / driver cannot report it.
293    pub memory_budget_bytes: u64,
294    /// Whether the GPU shares memory with the host.
295    pub unified_memory: bool,
296    /// Whether the GPU is a discrete card.
297    pub discrete: bool,
298}
299
300impl GpuProfile {
301    /// Conservative fallback for a backend that does not report a profile:
302    /// unknown hardware picks the cautious baseline, never a high preset. The
303    /// opposite default from `DeviceCapabilities::ALL` -- a feature gate fails
304    /// open (assume capable, no-op with a warning if not), but quality
305    /// auto-config fails safe (assume modest, never overdrive a weak GPU).
306    pub const UNKNOWN: Self = Self {
307        vendor: GpuVendor::Other,
308        tier: GpuTier::Unknown,
309        memory_budget_bytes: 0,
310        unified_memory: false,
311        discrete: false,
312    };
313}
314
315impl Default for GpuProfile {
316    fn default() -> Self {
317        Self::UNKNOWN
318    }
319}
320
321/// The cheap signals every backend can gather about its GPU, mapped to a coarse
322/// `GpuTier` by one shared rule so the three backends classify consistently and
323/// the mapping is unit-testable without a GPU. The backends differ in what they
324/// can report (Apple exposes a GPU family; DirectX / Vulkan expose a VRAM figure
325/// and a discrete / integrated flag), so this carries the union and the rule
326/// uses whichever signals are present.
327pub struct GpuClassInput {
328    /// The GPU's vendor.
329    pub vendor: GpuVendor,
330    /// Device memory the driver reports as budgeted for this process.
331    pub memory_budget_bytes: u64,
332    /// Whether the GPU is a discrete card.
333    pub discrete: bool,
334    /// Apple GPU family generation rank (7 = M1 .. 10 = M4), or 0 for a non-Apple
335    /// GPU. Apple silicon classifies by generation; everything else by VRAM.
336    pub apple_family: u8,
337}
338
339/// The Apple GPU family generation rank a device name implies, or 0 when the name
340/// is not an Apple silicon GPU. Metal reads the rank straight off the device
341/// (`MTLDevice::supportsFamily`); Vulkan has no equivalent query, so a MoltenVK
342/// build recovers it from the reported device name ("Apple M2 Max"). Without it
343/// Apple silicon falls through `classify_tier`'s integrated branch and the two
344/// backends disagree on the same GPU. `M<n>` maps to `n + 6`, matching Metal's
345/// `MTLGPUFamily::Apple7` = M1.
346pub fn apple_family_from_device_name(name: &str) -> u8 {
347    let Some(rest) = name.strip_prefix("Apple M") else {
348        return 0;
349    };
350    let digits: String = rest.chars().take_while(char::is_ascii_digit).collect();
351    match digits.parse::<u8>() {
352        Ok(n) if n >= 1 => n.saturating_add(6),
353        _ => 0,
354    }
355}
356
357/// Map the gathered GPU signals to a coarse performance tier. Apple silicon is
358/// classified by GPU family generation (family alone cannot separate base from
359/// Pro / Max / Ultra within a generation -- a working-set refinement can split
360/// them later); a non-Apple integrated / low-power GPU is the lowest tier; a
361/// discrete GPU is bucketed by dedicated VRAM. An unreporting device (no memory,
362/// not discrete) stays `Unknown` so the resolver uses the conservative baseline.
363pub fn classify_tier(input: &GpuClassInput) -> GpuTier {
364    const GB: u64 = 1 << 30;
365    // Apple silicon: classify by GPU family generation.
366    if input.vendor == GpuVendor::Apple && input.apple_family >= 7 {
367        return match input.apple_family {
368            7 => GpuTier::EntryDiscrete, // M1 class
369            8 => GpuTier::MidDiscrete,   // M2 class
370            _ => GpuTier::HighDiscrete,  // M3 / M4 and newer
371        };
372    }
373    // Any non-Apple integrated / low-power GPU is the lowest tier (Apple silicon
374    // is unified too, but it returned above via its family branch).
375    if !input.discrete {
376        return GpuTier::Integrated;
377    }
378    // Discrete GPU: bucket by dedicated VRAM.
379    match input.memory_budget_bytes {
380        0 => GpuTier::Unknown,
381        b if b >= 12 * GB => GpuTier::HighDiscrete,
382        b if b >= 6 * GB => GpuTier::MidDiscrete,
383        _ => GpuTier::EntryDiscrete,
384    }
385}
386
387/// The set of operations GraphicsSystem performs on a graphics backend.
388/// Implementations are thin forwarders to the inherent methods on
389/// MtlContext / DxContext / VkContext.
390///
391/// The asset hot-reload mutators below (`update_color_lut`,
392/// `rebuild_*_geometry`, `clone_static_draw_object`, etc.) are provided
393/// methods that default to a no-op, so a backend implements only the reload
394/// paths it actually supports.
395pub trait RenderBackend: SceneControl + Send {
396    /// Window / input lifecycle.
397    fn window_closed(&mut self) -> bool;
398    /// Confine the cursor to the window.
399    fn capture_cursor(&mut self);
400    /// Take the input sampled since the last call.
401    fn take_input(&mut self) -> RenderInput;
402    /// Block until the GPU has drained every submitted frame.
403    fn wait_idle(&self);
404
405    /// Per-frame drive. See [`FrameParams`] for the inputs.
406    fn draw_frame(&mut self, params: FrameParams<'_>) -> RenderResult<()>;
407    /// Push the camera's view matrix, column-major.
408    fn update_view(&mut self, matrix: [[f32; 4]; 4]);
409
410    /// Push this frame's changed model matrices, one `(draw slot, matrix)`
411    /// entry per moved draw object, applied in order. Batched so the trait is
412    /// crossed once per frame rather than once per entity; the caller sends
413    /// only slots whose matrix actually changed. An out-of-range slot is
414    /// ignored.
415    fn update_models(&mut self, updates: &[(u32, [[f32; 4]; 4])]);
416
417    /// Retire a draw object: hide it from every pass (main, shadow, velocity)
418    /// and exclude it from the ray-tracing acceleration structure, so a
419    /// despawned entity's slot leaves no ghost. The slot's geometry buffers are
420    /// untouched; the engine's draw-slot allocator returns the index to its
421    /// free list so a later `clone_static_draw_object` can recycle it. A no-op
422    /// if the index is out of range.
423    fn retire_draw_object(&mut self, draw_idx: usize);
424
425    /// Skinning. The skinned pipelines pair the world default Shader's
426    /// programs where the world declares one, which the backend kept from
427    /// init.
428    fn upload_skinned(
429        &mut self,
430        vertices: &[SkinnedVertex],
431        indices: &[u32],
432        draw_objects: Vec<SkinnedDrawObject>,
433    ) -> RenderResult<()>;
434    /// Push one skinned slot's joint matrices for this frame.
435    fn update_skinned_pose(&mut self, skinned_index: usize, matrices: &[[[f32; 4]; 4]]);
436
437    /// Attach morph-target data to the skinned draw objects, called once after
438    /// `upload_skinned`: `morphs[i]` belongs to draw object `i` (instance
439    /// copies share their template's data via the `Arc`). Default no-op for a
440    /// backend without a morph deformation path.
441    fn upload_skinned_morphs(
442        &mut self,
443        _morphs: Vec<Option<alloc::sync::Arc<crate::gfx::mesh_payload::PayloadMorphs>>>,
444    ) {
445    }
446
447    /// Push a skinned object's current morph-target weights, sampled by the
448    /// animation system each frame. A no-op when the index is out of range or
449    /// the object carries no morph targets.
450    fn update_morph_weights(&mut self, _skinned_index: usize, _weights: &[f32]) {}
451
452    // Runtime skinned spawn (pre-reserved instance pool): a backend pre-reserves
453    // hidden bind-pose copies at load (`SkinnedMesh.max_instances`) and reveals
454    // one per skinned SpawnRequest. The default no-op implementations are a
455    // fallback for a backend that has not wired runtime skinned spawn, where a
456    // skinned SpawnRequest finds nothing to claim and is dropped.
457
458    /// Reveal the pre-reserved skinned instance at `instance_index` (a hidden
459    /// bind-pose copy expanded at load): show it at `model` and reset its
460    /// palette to bind so it does not flash a previous occupant's pose. Which
461    /// instance to use is decided by the engine's instance pool; the backend
462    /// only applies it. A no-op if the index is out of range.
463    fn reveal_skinned_instance(&mut self, _instance_index: usize, _model: [[f32; 4]; 4]) {}
464
465    /// Hide a live skinned instance. The engine's instance pool returns the
466    /// slot for reuse; the backend only hides it. A no-op if the index is out
467    /// of range.
468    fn retire_skinned_draw_object(&mut self, _skinned_index: usize) {}
469
470    /// Push this frame's changed skinned model-to-world matrices, one
471    /// `(skinned index, matrix)` entry per moved instance, applied in order
472    /// (a skinned object animates in place unless something moves it). Cheap:
473    /// the per-frame cull rebuild reads the object's model directly, so this
474    /// just writes the fields. Out-of-range indices are ignored; default
475    /// no-op for a backend without movable skinned instances.
476    fn update_skinned_models(&mut self, _updates: &[(u32, [[f32; 4]; 4])]) {}
477
478    /// Texture streaming. Albedo and normal maps share one handle-indexed pool,
479    /// so every streamed texture (whatever its role) flows through these. The
480    /// image carries its GPU format and mip chain: RGBA8 regenerates mips on
481    /// upload, block-compressed formats upload their chain verbatim.
482    fn evict_texture_slot(&mut self, slot: usize) -> Result<(), String>;
483    /// Replace a texture slot's image after a streaming upload.
484    fn update_texture_slot(
485        &mut self,
486        slot: usize,
487        image: &crate::bake::texture::TextureImage,
488    ) -> RenderResult<()>;
489
490    /// Mesh streaming.
491    fn evict_mesh(&mut self, draw_idx: usize, retire_frame: u64) -> Result<(), String>;
492    /// Upload a streamed mesh's geometry into a draw slot.
493    fn upload_mesh(
494        &mut self,
495        draw_idx: usize,
496        verts: &[Vertex],
497        idxs: &[u16],
498        frame: u64,
499    ) -> RenderResult<()>;
500
501    /// Seed the streamed-mesh sub-allocators with one reserved headroom block
502    /// (byte ranges in the shared vertex / index buffers) instead of the
503    /// per-mesh build-time regions. Used by the shrinkable-seed path: the
504    /// streamed geometry is no longer baked into the buffers at build time, so
505    /// the renderer hands the allocators one contiguous block sized to the
506    /// cap-many resident meshes rather than the whole streamed set. Implemented
507    /// on Metal + DirectX + Vulkan. Default no-op: a backend without the
508    /// shrinkable seed keeps freeing each mesh's build-time region in
509    /// `setup_mesh_streaming`.
510    fn seed_mesh_streaming(
511        &mut self,
512        vtx_offset: u64,
513        vtx_bytes: u64,
514        idx_offset: u64,
515        idx_bytes: u64,
516    ) {
517        let _ = (vtx_offset, vtx_bytes, idx_offset, idx_bytes);
518    }
519
520    /// Voxel-world chunk streaming: grow the shared geometry buffers by the
521    /// chunk headroom. A chunk's material slots ride its own draw record.
522    fn setup_chunk_streaming(
523        &mut self,
524        chunk_vtx_bytes: usize,
525        chunk_idx_bytes: usize,
526    ) -> RenderResult<()>;
527    /// The destination draw slot comes from the engine's allocator, like
528    /// `clone_static_draw_object`; the freed slot is likewise returned to it by
529    /// the caller of `remove_chunk_mesh`.
530    fn add_chunk_mesh(
531        &mut self,
532        mesh: ChunkMesh<'_>,
533        dst: crate::render::draw_slot::SlotAlloc,
534    ) -> RenderResult<()>;
535    /// Free a streamed chunk's geometry, retiring it after `retire_frame`.
536    fn remove_chunk_mesh(&mut self, draw_idx: usize, retire_frame: u64) -> Result<(), String>;
537    /// Move a streamed chunk by replacing its placement matrix.
538    fn set_chunk_model(&mut self, draw_idx: usize, model: [[f32; 4]; 4]) -> Result<(), String>;
539
540    /// Device capability flags, queried from the GPU once the backend is built.
541    /// Read by GraphicsSystem to gray out + disable settings rows the device
542    /// cannot honor. Default: all capable, so a backend that does not report
543    /// capabilities keeps every toggle live (the feature then no-ops with a
544    /// warning on an incapable device, as before).
545    fn capabilities(&self) -> DeviceCapabilities {
546        DeviceCapabilities::ALL
547    }
548
549    /// Coarse GPU performance profile, queried once the backend is built. Read at
550    /// init to pick default graphics quality on first launch. Default: `UNKNOWN`
551    /// (the conservative tier), so a backend that does not report a profile never
552    /// makes the resolver auto-select a high preset.
553    fn gpu_profile(&self) -> GpuProfile {
554        GpuProfile::UNKNOWN
555    }
556
557    /// The overlay coordinate space: the window's content size in logical,
558    /// DPI-independent units (points on macOS, client pixels on Windows, window
559    /// coordinates on Linux). Every backend reports the cursor in these same
560    /// units, so UI hit-testing, text layout, and the overlay shader's divide to
561    /// NDC all share one space regardless of the backing scale. A backend
562    /// converts to attachment pixels only where a pixel rect is unavoidable,
563    /// through `fullscreen::clip_rect_to_scissor`.
564    ///
565    /// Default `(0.0, 0.0)` for a headless backend with no window.
566    fn logical_size(&self) -> (f32, f32) {
567        (0.0, 0.0)
568    }
569
570    /// Height of the window chrome overlapping the top of the render surface,
571    /// in the logical units `logical_size` reports. Non-zero only where the
572    /// content view runs under a transparent title bar (macOS), which leaves
573    /// the OS window buttons floating over the frame's top-left corner. UI that
574    /// must stay clear of them starts below this; the frame itself still covers
575    /// the whole window.
576    ///
577    /// Default `0.0`: a window whose content already begins below its chrome.
578    fn top_content_inset(&self) -> f32 {
579        0.0
580    }
581    /// Per-frame draw-call / object counters. Default no-op so a backend that
582    /// tracks none still satisfies the trait; all three shipping backends
583    /// override it.
584    fn render_stats(&self) -> RenderStats {
585        RenderStats::default()
586    }
587
588    /// Show or hide the OS cursor for an in-engine UI cursor (e.g. a MainMenu),
589    /// independent of camera capture. Edge-triggered by the backend, so calling
590    /// it every frame with the same value is cheap. Default no-op: a backend
591    /// without a free-mode cursor hide leaves the system cursor visible (DX /
592    /// Vulkan today).
593    fn set_ui_cursor_hidden(&mut self, hidden: bool) {
594        let _ = hidden;
595    }
596
597    /// Whether the real cursor has left the window, so an in-engine UI cursor
598    /// should stop drawing (windowed / borderless). The backend confines the
599    /// cursor to the active screen while in fullscreen, so it reports `false`
600    /// there. Default `false` (inside): backends without window-bounds tracking
601    /// (DX / Vulkan today) always draw the in-engine cursor.
602    fn cursor_outside_window(&self) -> bool {
603        false
604    }
605
606    /// Tell the backend a togglable menu (a Screen toggled by an Escape KeyBinding)
607    /// coexists with a captured camera. In this mode Escape routes to the ECS
608    /// (so the menu shows/hides) instead of releasing the cursor inline, and a
609    /// click never recaptures the cursor (it fires a UI action). Set once at
610    /// setup. Default no-op: backends without dynamic capture (DX / Vulkan today)
611    /// keep the static behavior.
612    fn set_menu_mode(&mut self, on: bool) {
613        let _ = on;
614    }
615
616    /// Drive cursor capture from the menu state each frame: capture for camera
617    /// control, release while a menu is open. Edge-triggered by the backend.
618    /// Default no-op (DX / Vulkan): they keep their startup capture decision.
619    fn set_camera_capture(&mut self, capture: bool) {
620        let _ = capture;
621    }
622
623    /// Supply the reflection-probe placements (from declared `ReflectionProbe`
624    /// assets, or empty to auto-seed from the scene bounds). The backend bakes a
625    /// cube per placement and samples the nearest for the specular reflection.
626    /// Pushed once after construction. Default no-op: backends without probe
627    /// support (DX / Vulkan today) keep the sky reflection.
628    fn set_reflection_probes(
629        &mut self,
630        probes: &[crate::render::reflection_probe::ProbePlacement],
631    ) {
632        let _ = probes;
633    }
634
635    /// Turn display sync (vsync) on or off at runtime, applied to presentation.
636    /// Edge-triggered by the backend, so calling it with the unchanged value is
637    /// cheap. Default no-op: a backend that only honors vsync at init ignores
638    /// runtime changes.
639    fn set_vsync(&mut self, on: bool) {
640        let _ = on;
641    }
642
643    /// Switch the window between windowed / borderless / fullscreen at runtime.
644    /// The change flows through the backend's normal resize path (no GPU rebuild
645    /// beyond the resize it triggers). Default no-op for backends without a
646    /// window (embedded / preview) or that don't yet implement it.
647    fn set_window_mode(&mut self, mode: crate::components::WindowMode) {
648        let _ = mode;
649    }
650
651    /// Resize the window's content area at runtime (meaningful in windowed mode).
652    /// Drives the same resize path as a user-dragged resize. Default no-op for
653    /// backends without a window or that don't yet implement it.
654    fn set_window_size(&mut self, width: u32, height: u32) {
655        let _ = (width, height);
656    }
657
658    /// The display modes (pixel resolution + refresh rate) the display this
659    /// backend renders to supports, unshaped (the caller dedups + sorts).
660    /// Default empty: a backend that cannot enumerate (or has no window) makes
661    /// the Resolution row fall back to the static preset list.
662    fn display_modes(&self) -> Vec<crate::render::display_mode::DisplayMode> {
663        Vec::new()
664    }
665
666    /// The mode the display is currently running, if the backend can read it.
667    /// Shown by the Resolution row when the user has never chosen a mode (the
668    /// display keeps its desktop mode until one is chosen). Default `None`.
669    fn current_display_mode(&self) -> Option<crate::render::display_mode::DisplayMode> {
670        None
671    }
672
673    /// Select the display mode to hold while the window is in fullscreen. The
674    /// backend applies it whenever the window is (or becomes) fullscreen and
675    /// restores the display's original mode when the window leaves fullscreen
676    /// or shuts down; outside fullscreen the choice is only remembered. Default
677    /// no-op: a backend without mode switching leaves the display alone.
678    fn set_display_mode(&mut self, mode: crate::render::display_mode::DisplayMode) {
679        let _ = mode;
680    }
681
682    /// Replace the live post-process tunables (bloom / exposure / vignette /
683    /// LUT blend / FXAA). These are pushed to the bloom + composite shaders each
684    /// frame, so a change takes effect on the next draw with no allocation or
685    /// pipeline rebuild. Only the authored half travels here: the composite's
686    /// display-output flags belong to the display the backend negotiated with
687    /// at init, so a push cannot disturb them. Default no-op: a backend that
688    /// only reads the tunables at init ignores runtime changes.
689    fn update_post_process(&mut self, tunables: PostProcessTunables) {
690        let _ = tunables;
691    }
692
693    /// Set the live ambient (IBL) light scale. Unlike the post-process params
694    /// above, `ambient_intensity` lives in the shared `LightUniforms` (uploaded
695    /// each frame by the main lighting pass), so it takes its own setter rather
696    /// than `update_post_process`. Default no-op: only Metal mutates it live
697    /// today; DirectX / Vulkan keep the init-time value (they read it at init).
698    fn set_ambient_intensity(&mut self, value: f32) {
699        let _ = value;
700    }
701
702    /// Replace the live directional-light set (the sun). Unlike the local
703    /// lights, which ride a per-scene storage buffer sized once at init, the
704    /// directional slots are a fixed-size array in the shared `LightUniforms`,
705    /// so a new set is written in place: the backend re-packs the array and
706    /// re-caches whatever it derived from the first light at init (the cascade
707    /// shadow direction, the fog sun). Default no-op: a backend that only reads
708    /// the lights at init keeps the init-time sun.
709    fn update_directional_lights(&mut self, lights: &[crate::components::DirectionalLight]) {
710        let _ = lights;
711    }
712
713    /// Push the gameplay movement key map. The backend resolves each canonical
714    /// `InputKey` to its native key code and decodes physical key events through the
715    /// map (instead of hardcoded keys), so a settings-menu rebind takes effect on
716    /// the next key event. Pushed once after the backend is built and again on
717    /// each rebind. Default no-op: a backend without keymap decode keeps its
718    /// built-in defaults.
719    fn set_keymap(&mut self, keymap: &KeyMap) {
720        let _ = keymap;
721    }
722
723    /// Apply a change to the quality-feature toggles (TAA / SSAO / SSR / RT
724    /// reflections / SSGI / auto-exposure) live. Unlike the post-process params,
725    /// these gate render passes whose GPU resources (pipelines, render targets,
726    /// ray-tracing acceleration structures) are built once at init, so applying a
727    /// change rebuilds the affected resources in place rather than flipping a
728    /// uniform. Default no-op: a backend that only reads these at init ignores
729    /// runtime changes (DirectX / Vulkan today), so the choice persists and takes
730    /// effect at the next launch there.
731    fn apply_quality_settings(&mut self, settings: QualitySettings) {
732        let _ = settings;
733    }
734
735    /// Set the shadow cascade re-render cadence live. The cascade scheduler reads
736    /// the policy at the start of each shadow pass, so a change takes effect on the
737    /// next draw with no pipeline rebuild or allocation (unlike the shadow map
738    /// resolution, which is sized once at init). Default no-op: a backend that only
739    /// reads the cadence at init keeps the init-time value (DirectX / Vulkan
740    /// today), so the choice persists and takes effect at the next launch there.
741    fn set_shadow_update(&mut self, update: crate::components::ShadowUpdate) {
742        let _ = update;
743    }
744
745    /// Set the shadow distance (world units the cascades cover, capped at the
746    /// camera far plane) live. The per-frame cascade-split computation reads it
747    /// each draw, so a change takes effect on the next frame with no allocation or
748    /// rebuild (it sizes no GPU resource, unlike the shadow map resolution).
749    /// Default no-op: a backend that only reads the distance at init keeps the
750    /// init-time value (DirectX / Vulkan today), so the choice persists and takes
751    /// effect at the next launch there.
752    fn set_shadow_distance(&mut self, distance: u32) {
753        let _ = distance;
754    }
755
756    /// Set the live shadow cascade count (1..=4). The cascade-split math + the
757    /// re-render schedule read it each frame and only the first `count` cascades
758    /// are projected, rendered, and sampled (the array capacity stays 4), so a
759    /// change takes effect on the next frame with no resize or rebuild. Default
760    /// no-op: a backend that only reads the count at init keeps the init-time
761    /// value (DirectX / Vulkan today), so the choice persists and takes effect at
762    /// the next launch there.
763    fn set_shadow_cascades(&mut self, count: u32) {
764        let _ = count;
765    }
766
767    /// Update the live scalar sub-tunables of the SSAO / SSR / SSGI / auto-exposure
768    /// passes (radius, intensity, distance, EV bounds, adaptation speed). Unlike
769    /// `apply_quality_settings`, this rebuilds nothing: each backend re-reads these
770    /// values from its stored `*Settings` structs into a per-frame uniform every
771    /// draw, so mutating them takes effect on the next frame with no pipeline /
772    /// target rebuild and no TAA-history reset. Only the fields of a feature that is
773    /// currently on are honoured (its settings are present); a value for an off
774    /// feature is ignored here and applies when the feature next turns on. The
775    /// structural sub-knobs (gather resolution, ray / step counts) are NOT live and
776    /// still ride `apply_quality_settings`. Default no-op: a backend that reads
777    /// these only at init keeps the init-time values (DirectX / Vulkan today), so
778    /// the choice persists and takes effect at the next launch there.
779    fn update_quality_params(&mut self, settings: QualitySettings) {
780        let _ = settings;
781    }
782
783    /// Shared atomic flag the backend polls at frame start to trigger a
784    /// shader rebuild. `Some` only under `cn debug` on backends that ship
785    /// hot-reload (Metal today); `None` on production runs and on backends
786    /// that have not implemented hot-reload yet. The debug server reads this
787    /// to forward `reload-shaders` commands; the filesystem watcher writes
788    /// it directly. Default: `None`.
789    fn shader_reload_flag(&self) -> Option<alloc::sync::Arc<core::sync::atomic::AtomicBool>> {
790        None
791    }
792
793    /// Replace the live colour-grading LUT with a fresh `size³` RGBA8 payload.
794    /// Driven by asset hot-reload (`cn debug` only). Default no-op: backends
795    /// that have not implemented the swap leave the LUT bound at whatever
796    /// payload was uploaded at init.
797    fn update_color_lut(&mut self, size: u32, data: &[u8]) -> Result<(), String> {
798        let _ = (size, data);
799        Ok(())
800    }
801
802    /// `(vertex_count, index_count)` for the static draw at `draw_idx`, or
803    /// `None` when the index is out of range / the backend does not expose
804    /// the field. Used by asset hot-reload to detect size-changing
805    /// reloads before attempting [`Self::update_mesh_geometry`], which
806    /// rejects size mismatches. Default returns `None`; backends that
807    /// implement the rebuild path also override this.
808    fn draw_geometry_size(&self, draw_idx: usize) -> Option<(usize, usize)> {
809        let _ = draw_idx;
810        None
811    }
812
813    /// Per-LOD-alternate index counts for the static draw at `draw_idx`,
814    /// ordered from LOD1 upward (LOD0 is reported by
815    /// [`Self::draw_geometry_size`]). Returns `None` when the index is out of
816    /// range or the backend does not expose its LOD layout. Used by asset
817    /// hot-reload alongside [`Self::draw_geometry_size`] to detect
818    /// size-changing reloads: a `.glb` that re-exports with a different LOD
819    /// breakdown queues the entry for [`Self::rebuild_static_geometry`]
820    /// instead of [`Self::update_mesh_geometry`]'s in-place write.
821    fn draw_lod_index_counts(&self, draw_idx: usize) -> Option<Vec<usize>> {
822        let _ = draw_idx;
823        None
824    }
825
826    /// Rebuild the shared static-mesh vertex + index buffers, replacing the
827    /// geometry of each `DrawGeometryUpdate.draw_idx` with the new
828    /// vertices / indices / LOD alternates. Draws not named in `changes`
829    /// keep their current geometry, copied byte-for-byte from the live
830    /// buffers. The slot's `vertex_count`, `index_count`, and
831    /// `lod_alternates` index offsets are rewritten as the new buffers are
832    /// laid out. Driven by asset hot-reload (`cn debug` only) when a
833    /// size-changing `.glb` re-export means the existing
834    /// [`Self::update_mesh_geometry`] in-place write no longer fits.
835    /// `wait_idle` first; the rebuild swaps the GPU buffers wholesale.
836    /// Default no-op: backends that have not implemented the rebuild
837    /// return `Ok(())` and the size-changing reload is logged + skipped at
838    /// the caller (the existing in-place path already errored on size
839    /// mismatch).
840    fn rebuild_static_geometry(&mut self, changes: Vec<DrawGeometryUpdate>) -> RenderResult<()> {
841        let _ = changes;
842        Ok(())
843    }
844
845    /// Replace a `SkinnedMesh` draw slot's vertex + index data in place.
846    /// Driven by asset hot-reload (`cn debug` only). Reuses the slot's
847    /// existing vertex region + index region in the shared skinned vertex /
848    /// index buffers (created once by [`Self::upload_skinned`]), so the new
849    /// geometry must match the slot's init-time vertex count + index count
850    /// and the new skeleton must keep the same joint count; pipelines stay
851    /// untouched, only the bytes change. `vertex_base` is the init-time
852    /// vertex offset (in vertex units) into the shared buffer; indices are
853    /// rebased onto it before writing. Default no-op.
854    fn update_skinned_mesh_geometry(
855        &mut self,
856        skinned_index: usize,
857        vertex_base: u32,
858        verts: &[SkinnedVertex],
859        idxs: &[u16],
860    ) -> Result<(), String> {
861        let _ = (skinned_index, vertex_base, verts, idxs);
862        Ok(())
863    }
864
865    /// Rebuild the shared skinned-mesh vertex + index buffers, replacing the
866    /// geometry of each `SkinnedDrawGeometryUpdate.skinned_index` with the
867    /// new vertices / indices. Slots not named in `changes` keep their
868    /// current geometry, copied byte-for-byte from the live buffers and
869    /// re-based onto the new vertex region they land in. Returns the
870    /// post-rebuild layout (one [`SkinnedSlotLayout`] per slot, in
871    /// `skinned_index` order) so the caller can refresh its source-map
872    /// `vertex_base` / `vertex_count` / `index_count` to point at the new
873    /// regions. Driven by asset hot-reload (`cn debug` only) when a
874    /// size-changing `.glb` re-export means the existing
875    /// [`Self::update_skinned_mesh_geometry`] in-place write no longer fits.
876    /// The backend `wait_idle`s first; the rebuild swaps the GPU buffers
877    /// wholesale. The skinned pipelines, shadow + velocity + SSAO + SSR
878    /// variants, and `skinned_draw_objects` slot metadata
879    /// (`texture_slot` / `normal_map_slot` / `material` / `joint_count`)
880    /// all stay untouched; only the `index_offset` / `index_count` on each
881    /// `SkinnedDrawObject` (and the buffers themselves) move. Default no-op
882    /// (returns an empty layout vec): backends that have not implemented
883    /// the rebuild leave the size-changing reload as logged + skipped at
884    /// the caller, the same behaviour as before, since the in-place path
885    /// already errored on size mismatch.
886    fn rebuild_skinned_geometry(
887        &mut self,
888        changes: Vec<SkinnedDrawGeometryUpdate>,
889    ) -> Result<Vec<SkinnedSlotLayout>, String> {
890        let _ = changes;
891        Ok(Vec::new())
892    }
893
894    /// Update a skinned slot's joint count and resize the backend's per-slot
895    /// joint-matrix buffers to match. Driven by asset hot-reload (`cn debug`
896    /// only) when a re-imported `.glb`'s skeleton has a different joint
897    /// count than the slot was initialised with. Shrinking truncates the
898    /// per-slot Vec; growing seeds the new entries to identity so the slot
899    /// renders undeformed on the next `update_skinned_pose`. The skinned
900    /// shaders consume the joints buffer through a pointer (not a fixed-
901    /// size array) and use vertex-attribute-encoded joint indices, so no
902    /// pipeline or shader rebuild is required for a joint-count change;
903    /// only the CPU-side per-slot buffer and `SkinnedDrawObject.joint_count`
904    /// change. Default no-op: backends that have not implemented the resize
905    /// leave the skeleton-shape change logged + skipped at the caller.
906    fn update_skinned_skeleton(
907        &mut self,
908        skinned_index: usize,
909        new_joint_count: usize,
910    ) -> Result<(), String> {
911        let _ = (skinned_index, new_joint_count);
912        Ok(())
913    }
914
915    /// Replace a `Mesh` draw slot's vertex + index data in place. Driven by
916    /// asset hot-reload (`cn debug` only). Reuses the slot's existing offset
917    /// in the shared vertex / index buffers, so the new geometry must match
918    /// the slot's init-time vertex count + index count; a size-changing
919    /// reload returns an error so the caller can queue
920    /// [`Self::rebuild_static_geometry`] instead, which repacks the shared
921    /// buffers. Each entry in
922    /// `lod_alternates` (`(switch_distance, mesh-relative indices)`) is
923    /// written to the matching slot's pre-allocated LOD index region; the
924    /// number of LODs and each LOD's index count must match the slot's
925    /// init-time layout, otherwise the call returns an error so the caller
926    /// can queue [`Self::rebuild_static_geometry`]. `switch_distance` is
927    /// re-stored per LOD so a JSON-side tweak to `lod_distances` propagates
928    /// without a process restart. Default no-op.
929    fn update_mesh_geometry(
930        &mut self,
931        draw_idx: usize,
932        verts: &[Vertex],
933        idxs: &[u16],
934        lod_alternates: &[(f32, Vec<u16>)],
935    ) -> Result<(), String> {
936        let _ = (draw_idx, verts, idxs, lod_alternates);
937        Ok(())
938    }
939
940    /// Replace the live IBL environment map with a freshly precomputed payload.
941    /// `payload` is the serialised byte format emitted by
942    /// `crate::bake::environment_map::compile_environment_map_payload`
943    /// (header + irradiance cube + prefilter mip chain), so init and hot-reload
944    /// share a single byte format. Driven by asset hot-reload (`cn debug`
945    /// only). Default no-op: backends that have not implemented the swap leave
946    /// the IBL cubes bound at whatever payload was uploaded at init.
947    fn update_environment_map(&mut self, payload: &[u8]) -> RenderResult<()> {
948        let _ = payload;
949        Ok(())
950    }
951
952    /// Replace the live volumetric-fog settings, or disable the fog pass when
953    /// `None`. Driven by world.jsonl hot-reload (`cn debug` only). Default
954    /// no-op: backends that have not implemented the swap leave the fog pass
955    /// at whatever settings were resolved at init.
956    ///
957    /// A backend that built its fog pipeline lazily based on the world's
958    /// init-time `VolumetricFog` cannot enable the pass via this call when
959    /// the world started with no fog declared; re-enabling fog on a world
960    /// that did not declare it at startup requires a relaunch.
961    fn update_fog_settings(&mut self, settings: Option<FogSettings>) {
962        let _ = settings;
963    }
964
965    /// Capture the last presented frame to a PNG at `path` and return the saved
966    /// path. Driven by the `cn debug` WS `screenshot` command for headless
967    /// on-GPU render verification. Default `Err`: a backend without a capture
968    /// path reports it unsupported (all current backends override this).
969    fn screenshot(&mut self, path: &str) -> Result<String, String> {
970        let _ = path;
971        Err("screenshot capture not supported on this backend".to_string())
972    }
973
974    /// Read the GPU-driven cull's per-object status buffer back to the host,
975    /// one [`crate::gfx::cull_status::CullStatus`] value per live cull record,
976    /// for the most recently submitted frame.
977    ///
978    /// The submitted draw-call count is a CPU-side number that does not move
979    /// when the GPU rejects an object, and an object the Hi-Z test correctly
980    /// occluded leaves no trace in the presented pixels, so this buffer is the
981    /// only observable record of what the cull decided. Driven by the `cn
982    /// debug` WS `cull-status` command; synchronous (it idles the device).
983    ///
984    /// Default `Err`: a backend with no GPU-driven cull, or one whose readback
985    /// path is not implemented, reports it unsupported.
986    fn read_cull_status(&mut self) -> Result<Vec<u32>, String> {
987        Err("cull-status readback not supported on this backend".to_string())
988    }
989
990    /// Instantiate a runtime copy of an existing draw object at a new transform:
991    /// re-use the source slot's geometry region (`vertex_offset` / `vertex_count`
992    /// / `index_offset` / `index_count` / `base_vertex` / `lod_alternates`) and
993    /// copy its texture slots, material, and cull distance, swapping only the
994    /// model matrix. The new slot reuses one freed by `retire_draw_object` before
995    /// growing the draw-object vec. The destination slot comes from the
996    /// engine's draw-slot allocator: `Reuse` overwrites a vacated entry,
997    /// `Append` grows the vec (the index always equals the current length,
998    /// which implementations debug-assert). Driven by runtime entity spawn
999    /// (`SpawnRequest`). The copy is non-cullable (sentinel AABB) and drawn
1000    /// every frame, since the init-time BVH cannot refit to admit a slot added
1001    /// at runtime; moving copies (the common case) opt out of the static BVH
1002    /// exactly like streamed chunks and held items. Default no-op (returns
1003    /// `Err`): backends without an implementation leave the spawn path
1004    /// logged + skipped at the caller.
1005    fn clone_static_draw_object(
1006        &mut self,
1007        src_draw_idx: usize,
1008        model: [[f32; 4]; 4],
1009        dst: crate::render::draw_slot::SlotAlloc,
1010    ) -> Result<(), String> {
1011        let _ = (src_draw_idx, model, dst);
1012        Err("clone_static_draw_object: not implemented on this backend".to_string())
1013    }
1014
1015    /// Rewrite a draw slot's material parameters + texture/normal-map pool
1016    /// indices in place. Driven by the editor's live draw seam when a Prop edits
1017    /// its `material` arg. Default no-op; a backend that implements it reports
1018    /// [`DeviceCapabilities::rewrites_draws`], which is what the caller gates on
1019    /// rather than pushing an edit that would not land.
1020    fn set_draw_material(
1021        &mut self,
1022        draw_idx: usize,
1023        material: MaterialUniforms,
1024        texture_slot: usize,
1025        normal_map_slot: usize,
1026    ) {
1027        let _ = (draw_idx, material, texture_slot, normal_map_slot);
1028    }
1029
1030    /// Rewrite a draw slot's `cull_distance` in place. Driven by the editor's
1031    /// live draw seam when a Prop edits its `cull_distance` arg. Default no-op,
1032    /// gated by the same [`DeviceCapabilities::rewrites_draws`] flag.
1033    fn set_draw_cull_distance(&mut self, draw_idx: usize, cull_distance: f32) {
1034        let _ = (draw_idx, cull_distance);
1035    }
1036
1037    /// Append a projected-decal record at runtime, returning a stable slot
1038    /// index the caller hands to [`Self::remove_decal`] later. Lets a
1039    /// gameplay system stamp bullet holes, footprints, or other ad-hoc
1040    /// decals after the world has built. Backends that have not implemented
1041    /// the runtime path return `Err`; the caller logs and drops the request.
1042    fn add_decal(&mut self, record: crate::render::decal::DecalRecord) -> Result<usize, String> {
1043        let _ = record;
1044        Err("add_decal: not implemented on this backend".to_string())
1045    }
1046
1047    /// Tombstone a runtime decal slot. The id returned by
1048    /// [`Self::add_decal`] becomes invalid; the next add may reuse it.
1049    /// Default no-op-with-Err: backends without a runtime path leave the
1050    /// remove logged + skipped at the caller.
1051    fn remove_decal(&mut self, decal_id: usize) -> Result<(), String> {
1052        let _ = decal_id;
1053        Err("remove_decal: not implemented on this backend".to_string())
1054    }
1055
1056    /// Append a particle-emitter record at runtime, returning a stable slot
1057    /// index. The backend allocates the per-emitter GPU pool + atomic
1058    /// spawn counter (matching the init-time path) so the compute kernel
1059    /// can begin ticking on the next frame. Default no-op-with-Err.
1060    fn add_emitter(
1061        &mut self,
1062        record: crate::render::particles::ParticleEmitterRecord,
1063    ) -> Result<usize, String> {
1064        let _ = record;
1065        Err("add_emitter: not implemented on this backend".to_string())
1066    }
1067
1068    /// Tombstone a runtime emitter slot and release its GPU pool +
1069    /// counter buffers (the GPU keeps them alive via its own refcount
1070    /// until any in-flight command buffer that referenced them completes).
1071    /// Default no-op-with-Err.
1072    fn remove_emitter(&mut self, emitter_id: usize) -> Result<(), String> {
1073        let _ = emitter_id;
1074        Err("remove_emitter: not implemented on this backend".to_string())
1075    }
1076
1077    /// Rebuild the live world-default pipelines (main, instanced, skinned)
1078    /// from a freshly compiled Shader payload. Driven by asset hot-reload
1079    /// (`cn debug` only) when one of the Shader's files is saved or a debug-WS
1080    /// `reload-assets` command fires. The backend builds every replacement
1081    /// into a temporary first and only swaps when every build succeeds, so a
1082    /// compile error never overwrites a live pipeline with a half-built
1083    /// replacement. Default `Err`: a backend without an implementation leaves
1084    /// the reload logged and skipped at the caller.
1085    fn update_world_shader_pipelines(&mut self, programs: &ShaderPrograms) -> Result<(), String> {
1086        let _ = programs;
1087        Err("update_world_shader_pipelines: not implemented on this backend".to_string())
1088    }
1089
1090    /// Build the render pipeline for one shader bucket from its compiled stage
1091    /// bytes, making draws that carry that bucket renderable. Called by the
1092    /// streaming pump when a scene that exclusively owns the bucket's `Shader`
1093    /// pins: init skipped the build, so this is where the cost lands (behind
1094    /// the loading screen, since the bucket counts as scene-resident content).
1095    /// Bucket 0 is the world default program and is never installed this way.
1096    ///
1097    /// Default no-op-with-Ok: a backend that renders every draw with the world
1098    /// default program has no per-bucket pipeline to build, and the bucket is
1099    /// resident as far as scene loading is concerned.
1100    fn install_world_shader(&mut self, bucket: u32, shader: WorldShader<'_>) -> RenderResult<()> {
1101        let _ = (bucket, shader);
1102        Ok(())
1103    }
1104
1105    /// Release one shader bucket's render pipeline, undoing
1106    /// [`Self::install_world_shader`]. Called when the owning scene unpins;
1107    /// draws carrying the bucket stop rendering until it is installed again.
1108    /// Default no-op, for the same reason as above.
1109    fn evict_world_shader(&mut self, bucket: u32) {
1110        let _ = bucket;
1111    }
1112
1113    /// The swapchain-level configuration this live backend can hot-swap a world
1114    /// onto, or `None` when the backend cannot reload a world in place (it must
1115    /// be fully rebuilt instead). Read by GraphicsSystem when a transplanted
1116    /// backend is handed a new world (the `cn editor` live SAVE): the swap reuses
1117    /// the backend via [`Self::reload_world`] only when this equals the new
1118    /// world's `BackendInit::swapchain_config`; a `None` or a mismatch routes to a
1119    /// full rebuild (recreating the window). Default `None`: DirectX / Vulkan
1120    /// (and any backend without a real `reload_world`) always rebuild.
1121    fn hot_swap_config(&self) -> Option<SwapchainConfig> {
1122        None
1123    }
1124
1125    /// Re-upload a new world's GPU content onto this already-constructed backend,
1126    /// reusing the live device + window + swapchain instead of building a new one.
1127    /// Driven by the `cn editor` live SAVE: after a structural edit recompiles the
1128    /// blobs, GraphicsSystem transplants the running backend into the rebuilt
1129    /// world and calls this so the edit applies without recreating the OS window
1130    /// or re-initialising the GPU device. The backend waits for the GPU to idle,
1131    /// drops the old world's content resources, and rebuilds them from `init` on
1132    /// the retained hardware. Only ever called when [`Self::hot_swap_config`]
1133    /// reported a config matching `init.swapchain_config()`, so the swapchain
1134    /// (pixel format / frames-in-flight / EDR) is guaranteed unchanged. Default
1135    /// `Err`/unsupported: DirectX / Vulkan fall back to a full rebuild (no
1136    /// regression; a real implementation is Windows-pending like the rest).
1137    fn reload_world(&mut self, init: BackendInit<'_>) -> RenderResult<()> {
1138        let _ = init;
1139        Err(RenderError::Other(
1140            "reload_world: not supported on this backend".to_string(),
1141        ))
1142    }
1143}
1144
1145// A do-nothing backend used to exercise the trait's provided (default) method
1146// bodies without a GPU: the smallest valid bodies for the required methods,
1147// no defaults overridden. Shared by this module's tests and the ops tests.
1148#[cfg(test)]
1149pub(crate) mod test_stub {
1150    use super::*;
1151
1152    pub(crate) struct StubBackend;
1153
1154    impl SceneControl for StubBackend {
1155        fn update_visibility(&mut self, _draw_idx: usize, _visible: bool) {}
1156        fn set_fade(&mut self, _fade: f32) {}
1157    }
1158
1159    impl RenderBackend for StubBackend {
1160        fn window_closed(&mut self) -> bool {
1161            false
1162        }
1163        fn capture_cursor(&mut self) {}
1164        fn take_input(&mut self) -> RenderInput {
1165            RenderInput::default()
1166        }
1167        fn wait_idle(&self) {}
1168        fn draw_frame(&mut self, _params: FrameParams<'_>) -> RenderResult<()> {
1169            Ok(())
1170        }
1171        fn update_view(&mut self, _matrix: [[f32; 4]; 4]) {}
1172        fn update_models(&mut self, _updates: &[(u32, [[f32; 4]; 4])]) {}
1173        fn retire_draw_object(&mut self, _draw_idx: usize) {}
1174        fn upload_skinned(
1175            &mut self,
1176            _vertices: &[SkinnedVertex],
1177            _indices: &[u32],
1178            _draw_objects: Vec<SkinnedDrawObject>,
1179        ) -> RenderResult<()> {
1180            Ok(())
1181        }
1182        fn update_skinned_pose(&mut self, _skinned_index: usize, _matrices: &[[[f32; 4]; 4]]) {}
1183        fn evict_texture_slot(&mut self, _slot: usize) -> Result<(), String> {
1184            Ok(())
1185        }
1186        fn update_texture_slot(
1187            &mut self,
1188            _slot: usize,
1189            _image: &crate::bake::texture::TextureImage,
1190        ) -> RenderResult<()> {
1191            Ok(())
1192        }
1193        fn evict_mesh(&mut self, _draw_idx: usize, _retire_frame: u64) -> Result<(), String> {
1194            Ok(())
1195        }
1196        fn upload_mesh(
1197            &mut self,
1198            _draw_idx: usize,
1199            _verts: &[Vertex],
1200            _idxs: &[u16],
1201            _frame: u64,
1202        ) -> RenderResult<()> {
1203            Ok(())
1204        }
1205        fn setup_chunk_streaming(
1206            &mut self,
1207            _chunk_vtx_bytes: usize,
1208            _chunk_idx_bytes: usize,
1209        ) -> RenderResult<()> {
1210            Ok(())
1211        }
1212        fn add_chunk_mesh(
1213            &mut self,
1214            _mesh: ChunkMesh<'_>,
1215            _dst: crate::render::draw_slot::SlotAlloc,
1216        ) -> RenderResult<()> {
1217            Ok(())
1218        }
1219        fn remove_chunk_mesh(
1220            &mut self,
1221            _draw_idx: usize,
1222            _retire_frame: u64,
1223        ) -> Result<(), String> {
1224            Ok(())
1225        }
1226        fn set_chunk_model(
1227            &mut self,
1228            _draw_idx: usize,
1229            _model: [[f32; 4]; 4],
1230        ) -> Result<(), String> {
1231            Ok(())
1232        }
1233    }
1234}
1235
1236#[cfg(test)]
1237mod tests {
1238    use super::*;
1239
1240    use alloc::vec;
1241    const GB: u64 = 1 << 30;
1242
1243    fn input(
1244        vendor: GpuVendor,
1245        memory_budget_bytes: u64,
1246        discrete: bool,
1247        apple_family: u8,
1248    ) -> GpuClassInput {
1249        GpuClassInput {
1250            vendor,
1251            memory_budget_bytes,
1252            discrete,
1253            apple_family,
1254        }
1255    }
1256
1257    #[test]
1258    fn unknown_profile_is_the_conservative_default() {
1259        // The opposite default from capabilities: quality auto-config fails safe.
1260        let p = GpuProfile::default();
1261        assert_eq!(p.tier, GpuTier::Unknown);
1262        assert_eq!(p.vendor, GpuVendor::Other);
1263        assert_eq!(p.memory_budget_bytes, 0);
1264        // Unknown sorts below every real tier, so a `>=` resolver treats it as
1265        // the floor.
1266        assert!(GpuTier::Unknown < GpuTier::Integrated);
1267        assert!(GpuTier::Integrated < GpuTier::EntryDiscrete);
1268        assert!(GpuTier::EntryDiscrete < GpuTier::MidDiscrete);
1269        assert!(GpuTier::MidDiscrete < GpuTier::HighDiscrete);
1270    }
1271
1272    #[test]
1273    fn apple_family_reads_the_generation_out_of_the_device_name() {
1274        // The names MoltenVK reports, mapped onto Metal's family ranks.
1275        assert_eq!(apple_family_from_device_name("Apple M1"), 7);
1276        assert_eq!(apple_family_from_device_name("Apple M2 Max"), 8);
1277        assert_eq!(apple_family_from_device_name("Apple M3 Pro"), 9);
1278        assert_eq!(apple_family_from_device_name("Apple M4 Ultra"), 10);
1279        // A generation past what Metal's SDK names yet still ranks above M3, so
1280        // a newer Mac is not demoted.
1281        assert!(apple_family_from_device_name("Apple M9") > 9);
1282    }
1283
1284    #[test]
1285    fn non_apple_device_names_report_no_family() {
1286        for name in [
1287            "NVIDIA GeForce RTX 4090",
1288            "AMD Radeon RX 7900 XTX",
1289            "Intel(R) Arc(tm) A770",
1290            // Apple's own non-M naming, and a truncated / malformed report.
1291            "Apple A17 Pro",
1292            "Apple M",
1293            "Apple MX",
1294            "",
1295        ] {
1296            assert_eq!(apple_family_from_device_name(name), 0, "{name}");
1297        }
1298    }
1299
1300    #[test]
1301    fn apple_family_from_a_name_reaches_the_same_tier_metal_does() {
1302        // The whole point of the name probe: a MoltenVK build must land on the
1303        // tier the Metal backend reports for the same silicon, not on the
1304        // integrated floor a zero family falls through to.
1305        let family = apple_family_from_device_name("Apple M2 Max");
1306        assert_eq!(
1307            classify_tier(&input(GpuVendor::Apple, 32 * GB, false, family)),
1308            GpuTier::MidDiscrete
1309        );
1310        assert_eq!(
1311            classify_tier(&input(GpuVendor::Apple, 32 * GB, false, 0)),
1312            GpuTier::Integrated
1313        );
1314    }
1315
1316    #[test]
1317    fn apple_silicon_classifies_by_generation() {
1318        // Unified memory is large on Apple silicon, but the family generation
1319        // (not the working-set) decides the tier, so the huge shared budget does
1320        // not read as a high-VRAM discrete card.
1321        assert_eq!(
1322            classify_tier(&input(GpuVendor::Apple, 16 * GB, false, 7)),
1323            GpuTier::EntryDiscrete // M1
1324        );
1325        assert_eq!(
1326            classify_tier(&input(GpuVendor::Apple, 24 * GB, false, 8)),
1327            GpuTier::MidDiscrete // M2
1328        );
1329        assert_eq!(
1330            classify_tier(&input(GpuVendor::Apple, 48 * GB, false, 9)),
1331            GpuTier::HighDiscrete // M3
1332        );
1333        assert_eq!(
1334            classify_tier(&input(GpuVendor::Apple, 64 * GB, false, 10)),
1335            GpuTier::HighDiscrete // M4 and newer cap at high
1336        );
1337    }
1338
1339    #[test]
1340    fn discrete_gpu_classifies_by_vram() {
1341        // An Intel-Mac AMD dGPU or a PC discrete card: vendor is not Apple and
1342        // there is no Apple family, so VRAM buckets the tier.
1343        assert_eq!(
1344            classify_tier(&input(GpuVendor::Nvidia, 24 * GB, true, 0)),
1345            GpuTier::HighDiscrete
1346        );
1347        assert_eq!(
1348            classify_tier(&input(GpuVendor::Amd, 8 * GB, true, 0)),
1349            GpuTier::MidDiscrete
1350        );
1351        assert_eq!(
1352            classify_tier(&input(GpuVendor::Nvidia, 4 * GB, true, 0)),
1353            GpuTier::EntryDiscrete
1354        );
1355        // A discrete card that reports no memory budget is left Unknown rather
1356        // than guessed high.
1357        assert_eq!(
1358            classify_tier(&input(GpuVendor::Amd, 0, true, 0)),
1359            GpuTier::Unknown
1360        );
1361    }
1362
1363    #[test]
1364    fn integrated_gpu_is_the_lowest_tier() {
1365        // Non-Apple integrated part: no dedicated memory, not unified, no Apple
1366        // family.
1367        assert_eq!(
1368            classify_tier(&input(GpuVendor::Intel, 0, false, 0)),
1369            GpuTier::Integrated
1370        );
1371    }
1372
1373    #[test]
1374    fn vram_bucket_boundaries() {
1375        // Boundaries are inclusive lower bounds (>= 12 GB high, >= 6 GB mid).
1376        assert_eq!(
1377            classify_tier(&input(GpuVendor::Nvidia, 12 * GB, true, 0)),
1378            GpuTier::HighDiscrete
1379        );
1380        assert_eq!(
1381            classify_tier(&input(GpuVendor::Nvidia, 12 * GB - 1, true, 0)),
1382            GpuTier::MidDiscrete
1383        );
1384        assert_eq!(
1385            classify_tier(&input(GpuVendor::Nvidia, 6 * GB, true, 0)),
1386            GpuTier::MidDiscrete
1387        );
1388        assert_eq!(
1389            classify_tier(&input(GpuVendor::Nvidia, 6 * GB - 1, true, 0)),
1390            GpuTier::EntryDiscrete
1391        );
1392    }
1393
1394    pub(crate) use super::test_stub::StubBackend;
1395
1396    const IDENTITY: [[f32; 4]; 4] = [
1397        [1.0, 0.0, 0.0, 0.0],
1398        [0.0, 1.0, 0.0, 0.0],
1399        [0.0, 0.0, 1.0, 0.0],
1400        [0.0, 0.0, 0.0, 1.0],
1401    ];
1402
1403    // Minimal QualitySettings with every feature off, so no *Settings sub-type
1404    // needs constructing.
1405    fn stub_quality() -> QualitySettings {
1406        QualitySettings {
1407            taa: false,
1408            ssao: None,
1409            ssr: None,
1410            rt_reflections: None,
1411            ssgi: None,
1412            reflection_blur_scale: 1,
1413            auto_exposure: None,
1414            auto_exposure_bias_ev: 0.0,
1415        }
1416    }
1417
1418    #[test]
1419    fn default_query_methods_report_conservative_values() {
1420        let backend = StubBackend;
1421        // Capabilities fail open: a backend that does not report keeps every
1422        // toggle live.
1423        assert!(backend.capabilities().ray_tracing);
1424        // Quality auto-config fails safe: the unknown/conservative profile.
1425        assert_eq!(backend.gpu_profile().tier, GpuTier::Unknown);
1426        assert_eq!(backend.gpu_profile().vendor, GpuVendor::Other);
1427        assert_eq!(backend.gpu_profile().memory_budget_bytes, 0);
1428        // Diagnostics a backend may leave to the default: zeroed here.
1429        assert_eq!(backend.logical_size(), (0.0, 0.0));
1430        // No chrome over the frame: UI anchored to the top starts at the top.
1431        assert_eq!(backend.top_content_inset(), 0.0);
1432        assert_eq!(backend.render_stats(), RenderStats::default());
1433        // No window-bounds tracking: the in-engine cursor always draws.
1434        assert!(!backend.cursor_outside_window());
1435        // No display enumeration and no hot-reload flag wired.
1436        assert!(backend.display_modes().is_empty());
1437        assert!(backend.current_display_mode().is_none());
1438        assert!(backend.shader_reload_flag().is_none());
1439        // Not hot-swap-capable: a live world reload routes to a full rebuild.
1440        assert!(backend.hot_swap_config().is_none());
1441        // No geometry-size introspection for the reload size check.
1442        assert!(backend.draw_geometry_size(0).is_none());
1443        assert!(backend.draw_lod_index_counts(0).is_none());
1444    }
1445
1446    #[test]
1447    fn default_mutators_are_noops_and_fallible_hooks_report_defaults() {
1448        let mut backend = StubBackend;
1449
1450        // Runtime skinned-spawn fallbacks: nothing to reveal or hide.
1451        backend.reveal_skinned_instance(0, IDENTITY);
1452        backend.retire_skinned_draw_object(0);
1453        backend.update_skinned_models(&[(0, IDENTITY)]);
1454
1455        // Streaming + cursor + capture no-ops.
1456        backend.seed_mesh_streaming(0, 0, 0, 0);
1457        backend.set_ui_cursor_hidden(true);
1458        backend.set_menu_mode(true);
1459        backend.set_camera_capture(true);
1460        backend.set_reflection_probes(&[]);
1461
1462        // Presentation + window no-ops.
1463        backend.set_vsync(true);
1464        backend.set_window_mode(crate::components::WindowMode::Fullscreen);
1465        backend.set_window_size(1280, 720);
1466        backend.set_display_mode(crate::render::display_mode::DisplayMode {
1467            width: 1920,
1468            height: 1080,
1469            refresh_hz: 60,
1470        });
1471
1472        // Live look + input tunable no-ops.
1473        backend.update_post_process(PostProcessTunables::DEFAULT);
1474        backend.set_ambient_intensity(1.0);
1475        backend.set_keymap(&KeyMap::default());
1476        backend.apply_quality_settings(stub_quality());
1477        backend.update_quality_params(stub_quality());
1478        backend.set_shadow_update(crate::components::ShadowUpdate::EveryFrame);
1479        backend.set_shadow_distance(200);
1480        backend.set_shadow_cascades(3);
1481        backend.update_fog_settings(None);
1482        backend.update_directional_lights(&[]);
1483        backend.set_draw_material(0, MaterialUniforms::DEFAULT, 0, 0);
1484        backend.set_draw_cull_distance(0, 50.0);
1485
1486        // Fallible hot-reload hooks that succeed by default (no-op Ok).
1487        assert!(backend.update_color_lut(2, &[0u8; 32]).is_ok());
1488        assert!(backend.rebuild_static_geometry(vec![]).is_ok());
1489        assert!(backend.update_skinned_mesh_geometry(0, 0, &[], &[]).is_ok());
1490        assert!(backend.rebuild_skinned_geometry(vec![]).unwrap().is_empty());
1491        assert!(backend.update_skinned_skeleton(0, 0).is_ok());
1492        assert!(backend.update_mesh_geometry(0, &[], &[], &[]).is_ok());
1493        assert!(backend.update_environment_map(&[]).is_ok());
1494
1495        // Fallible hooks a bare backend does not implement: they report Err.
1496        assert!(backend.screenshot("unused.png").is_err());
1497        assert!(
1498            backend
1499                .clone_static_draw_object(
1500                    0,
1501                    IDENTITY,
1502                    crate::render::draw_slot::SlotAlloc::Append(0)
1503                )
1504                .is_err()
1505        );
1506        assert!(backend.add_decal(stub_decal()).is_err());
1507        assert!(backend.remove_decal(0).is_err());
1508        assert!(backend.add_emitter(stub_emitter()).is_err());
1509        assert!(backend.remove_emitter(0).is_err());
1510        assert!(
1511            backend
1512                .update_world_shader_pipelines(&ShaderPrograms::default())
1513                .is_err()
1514        );
1515    }
1516
1517    // A minimal empty-world BackendInit borrowing `window`, for exercising the
1518    // default `reload_world`. Empty slices are `'static`; the only real borrow
1519    // is the window args.
1520    fn empty_backend_init(window: &crate::components::Window) -> BackendInit<'_> {
1521        BackendInit::minimal(window, alloc::vec::Vec::new())
1522    }
1523
1524    #[test]
1525    fn default_reload_world_is_unsupported() {
1526        // A backend without a real reload path reports the swap unsupported, so
1527        // the caller falls back to a full rebuild.
1528        let mut backend = StubBackend;
1529        let window = crate::components::Window::default();
1530        assert!(backend.reload_world(empty_backend_init(&window)).is_err());
1531    }
1532
1533    fn stub_decal() -> crate::render::decal::DecalRecord {
1534        crate::render::decal::DecalRecord {
1535            model: IDENTITY,
1536            inv_model: IDENTITY,
1537            texture_slot: 0,
1538            tint: [1.0; 4],
1539        }
1540    }
1541
1542    fn stub_emitter() -> crate::render::particles::ParticleEmitterRecord {
1543        crate::render::particles::ParticleEmitterRecord {
1544            texture_slot: 0,
1545            position: [0.0; 3],
1546            direction: [0.0, 1.0, 0.0],
1547            spread_cos: 1.0,
1548            speed_min: 0.0,
1549            speed_max: 1.0,
1550            lifetime_min: 0.0,
1551            lifetime_max: 1.0,
1552            gravity: [0.0, -9.8, 0.0],
1553            spawn_rate: 1.0,
1554            max_particles: 1,
1555            size_start: 1.0,
1556            size_end: 1.0,
1557            color_start: [1.0; 4],
1558            color_end: [1.0; 4],
1559        }
1560    }
1561}