pub struct GpuRenderer { /* private fields */ }Expand description
WGPU-backed renderer bound to a host window: owns the device,
queue, surface, and every lazily-built pass (multi-grid scene DDA,
sprite DDA, resolve/posterize, overlays, egui HUD).
Self::render_scene marches the frame; Self::present shows
it. Construct with Self::new / Self::new_blocking and fall
back to the CPU path on error.
The window handle is consumed only at construction — wgpu’s
Surface<'static> keeps its own Arc clone, so the renderer holds
no window field of its own.
Implementations§
Source§impl GpuRenderer
impl GpuRenderer
Sourcepub fn set_scene_lights(&mut self, lights: SceneLights)
pub fn set_scene_lights(&mut self, lights: SceneLights)
DL — set the per-frame dynamic lights (sun + point lights), already
transformed into each grid’s local frame. Call once per frame before
Self::render_scene (the facade does this from
FrameParams::lights). SceneLights::default clears all lights —
the pre-DL render. GPU-only; the CPU backend has no analogue.
PF.5 — an unchanged rig is a no-op: render_scene re-packs +
re-uploads the light buffers only when this actually stores
something different, so a static rig costs nothing per frame.
Source§impl GpuRenderer
impl GpuRenderer
Sourcepub fn draw_lines_deferred(&mut self, cam: &GpuLineCamera, lines: &[GpuLine])
pub fn draw_lines_deferred(&mut self, cam: &GpuLineCamera, lines: &[GpuLine])
Draw depth-tested world-space GpuLines over the pending frame
(L3.2). Projects each endpoint with cam (the marcher’s pinhole) +
the last frame’s FOV / surface size, expands to screen-space quads,
and runs a LoadOp::Load pass into the pending swapchain view — so
the lines land on the marched frame and a later present /
paint_egui still finishes it (the pending frame is left intact).
Depth-tested lines are occluded by nearer marched geometry (compared
against the scene-DDA depth buffer’s best_t); call after render,
before present / paint_egui. No-op if no frame is pending.
Sourcepub fn upload_image(&mut self, rgba: &[u8], width: u32, height: u32) -> usize
pub fn upload_image(&mut self, rgba: &[u8], width: u32, height: u32) -> usize
Upload (or replace) an RGBA8 image as a sampled texture, returning
a stable id for GpuImageQuad::image. rgba is row-major,
width * height * 4 bytes, straight (un-premultiplied) alpha.
Reuses a dropped slot when one exists. Returns 0 for malformed
input (an id that draws nothing).
Sourcepub fn drop_image(&mut self, id: usize)
pub fn drop_image(&mut self, id: usize)
Release an image uploaded with Self::upload_image (the slot
becomes reusable).
Sourcepub fn draw_images_deferred(
&mut self,
cam: &GpuLineCamera,
quads: &[GpuImageQuad],
)
pub fn draw_images_deferred( &mut self, cam: &GpuLineCamera, quads: &[GpuImageQuad], )
Draw world-space 2D image sprites (GpuImageQuad) over the
pending frame — the textured-quad sibling of
Self::draw_lines_deferred. Projects each quad with cam (the
marcher’s pinhole) + the last frame’s FOV / surface size, expands +
near-clips to triangles, and runs one LoadOp::Load pass with a
draw per quad (each binds its own texture). UVs are perspective-correct;
depth-tested quads are occluded by nearer marched geometry. Call
after render, before present / paint_egui. No-op if no frame
is pending.
Source§impl GpuRenderer
impl GpuRenderer
Sourcepub fn read_depth_pixel(&self, x: u32, y: u32) -> Option<f32>
pub fn read_depth_pixel(&self, x: u32, y: u32) -> Option<f32>
Read back the per-pixel world-t depth at window pixel (x, y)
from the last rendered frame, for screen→world picking. Returns
the distance t along the (normalised) view ray to the nearest
scene-grid surface, so the host reconstructs the world hit as
cam.pos + t * normalize(ray_dir). None for out-of-bounds
pixels, sky / no-hit (the T_INF sentinel), or when no scene
frame has been rendered.
The depth buffer is the SCENE pass’s output (terrain + grids), untouched by the sprite pass (which reads it read-only), so a cursor sprite under the pointer does not occlude the pick.
Synchronous: copies the depth buffer to a mapped staging buffer
and blocks on device.poll(Wait). Cheap enough for click-time
picks; do not call it every frame.
The scene pass always writes depth (L3.1), so the last rendered frame is pickable with or without sprites in it.
Compiles on wasm, but the wasm facade never calls it: WebGPU’s
device.poll doesn’t block for the GPU, so the blocking
recv() here would hang the single browser thread. The wasm
facade calls Self::read_depth_pixel_async instead (PW.1 —
one-frame latency).
Sourcepub fn read_depth_pixel_async(&self, x: u32, y: u32) -> Option<f32>
pub fn read_depth_pixel_async(&self, x: u32, y: u32) -> Option<f32>
PW.1 — the async counterpart of Self::read_depth_pixel for
the wasm GPU path, where map_async only resolves on browser
event-loop turns and blocking would hang the single thread.
Each call: (1) harvests the previous readback if its map
has resolved (the browser resolves it between RAF frames), (2)
re-arms — submits a fresh 4-byte copy + map for THIS call’s
pixel if nothing is in flight (clicks arriving while one is
mapping are coalesced away; the next call re-arms with its own,
newest pixel), and (3) returns the latest completed depth —
usually None on the first call and the value on the next
(one-frame latency; the result may correspond to the previously
requested pixel). Same T_INF/non-finite sky filtering as the
sync path.
The staging buffer is created per pick (4 bytes) and owned by
the pick state, NOT the shared depth_readback: the copy
executes against the depth buffer at submit time, so a resize /
scene swap between calls cannot invalidate an in-flight pick.
Compiles and works on every target (the state machine
unit-tests natively), but native hosts should call the sync
Self::read_depth_pixel: without the browser event loop the
map only resolves if something polls the device between calls.
Sourcepub fn read_frame_pixels(&self) -> Option<(Vec<u32>, u32, u32)>
pub fn read_frame_pixels(&self) -> Option<(Vec<u32>, u32, u32)>
QE.7a — read back the last rendered frame’s colour at the
logical resolution (post-SSAA/posterize, pre-upscale) as
0x00RRGGBB pixels — the GPU side of frame capture, closing
the “screenshots impossible on the GPU backend” parity gap.
Blocking (encode copy → submit → map, like
Self::read_depth_pixel): a screenshot hotkey, not a
per-frame path. None before the first scene render. Compiles
on wasm but must not be called there — WebGPU’s poll can’t
block, so the facade returns None on the wasm GPU path.
Sourcepub fn pixel_ray(
&self,
right: [f64; 3],
down: [f64; 3],
forward: [f64; 3],
x: f64,
y: f64,
) -> Option<[f64; 3]>
pub fn pixel_ray( &self, right: [f64; 3], down: [f64; 3], forward: [f64; 3], x: f64, y: f64, ) -> Option<[f64; 3]>
World-space view-ray direction (un-normalised) for window pixel
(x, y), under the GPU marcher’s projection — the canonical GPU
unproject, mirroring scene_dda.wgsl’s render_scene
(vertical-FOV pinhole). Uses the last-rendered frame’s target
size + FOV; None before the first scene render. Pair with
Self::read_depth_pixel for screen→world picking.
Source§impl GpuRenderer
impl GpuRenderer
Sourcepub async fn new<W>(
window: Arc<W>,
size: (u32, u32),
settings: GpuRendererSettings,
) -> Result<Self, GpuInitError>
pub async fn new<W>( window: Arc<W>, size: (u32, u32), settings: GpuRendererSettings, ) -> Result<Self, GpuInitError>
Stand up the device + surface + swapchain on window. Async
because wgpu::Adapter/Device requests are.
window is any raw-window-handle provider (winit, SDL,
GLFW, …) wrapped in an Arc; size is its initial physical
framebuffer size in pixels — passed explicitly so the renderer
stays decoupled from any one windowing library’s size API.
§Errors
Returns GpuInitError if surface creation, adapter
selection, or device request fails. Hosts treat any error as
“fall back to the CPU path”.
Sourcepub fn new_blocking<W>(
window: Arc<W>,
size: (u32, u32),
settings: GpuRendererSettings,
) -> Result<Self, GpuInitError>
pub fn new_blocking<W>( window: Arc<W>, size: (u32, u32), settings: GpuRendererSettings, ) -> Result<Self, GpuInitError>
Sourcepub fn adapter_info(&self) -> &str
pub fn adapter_info(&self) -> &str
Human-readable adapter description — name + backend + device type. The demo host prints this in the title bar.
Sourcepub fn low_power(&self) -> bool
pub fn low_power(&self) -> bool
true when the adapter is NOT a discrete GPU (integrated,
software rasterizer, virtual, unknown) — a hint that hosts
should default to a lighter render resolution.
Sourcepub fn device(&self) -> &Device
pub fn device(&self) -> &Device
Borrow the underlying wgpu device — hosts use this to build
chunk uploads (GpuChunkResident::upload(gpu.device(), …)).
Sourcepub fn sprite_shadows_capable(&self) -> bool
pub fn sprite_shadows_capable(&self) -> bool
XS.4 — whether this device can run GPU sprite shadows (it granted
enough storage buffers per shader stage for the cross-pass occupancy
bindings). false ⇒ GPU sprites render unshadowed; the CPU backend
always has sprite shadows. Lets the facade/host report the fallback.
Sourcepub fn queue(&self) -> &Queue
pub fn queue(&self) -> &Queue
Borrow the wgpu queue — hosts use this for read-back paths
(GpuChunkResident::read_voxel_blocking(gpu.device(), gpu.queue(), …)).
Sourcepub fn set_flip_x(&mut self, flip: bool)
pub fn set_flip_x(&mut self, flip: bool)
GPU.8 — upload an equirectangular panorama as the scene’s
sky texture. rgba is row-major, width × height pixels,
4 bytes per pixel (R, G, B, A). The shader samples it with
u = atan2(dir.x, dir.y) / (2π) + 0.5 (azimuth) and
v = acos(-dir.z) / π (elevation), matching standard
equirectangular layout (top of image = zenith for voxlap’s
+z = down basis).
Mirror the marched scene (and its line/image overlays) horizontally
on present, leaving the egui overlay upright. See Self::flip_x.
Sourcepub fn set_sky_panorama(&mut self, rgba: &[u8], width: u32, height: u32)
pub fn set_sky_panorama(&mut self, rgba: &[u8], width: u32, height: u32)
§Panics
If rgba.len() != (width * height * 4) as usize.
Sourcepub fn set_fog(&mut self, color: [f32; 3], near: f32, far: f32)
pub fn set_fog(&mut self, color: [f32; 3], near: f32, far: f32)
GPU.8 — set the fog blend. color is per-channel [0, 1];
near/far are world-space ray distances in voxel units.
Hits with t < near show their full colour; hits with
t > far show color exclusively; in between is a
smoothstep blend.
Sourcepub fn resize(&mut self, width: u32, height: u32)
pub fn resize(&mut self, width: u32, height: u32)
Re-configure the swapchain to a new physical size. Call from
WindowEvent::Resized. The scene resources rebuild lazily at
the new size on the next Self::render_scene.
Sourcepub fn set_render_resolution(&mut self, res: RenderResolution)
pub fn set_render_resolution(&mut self, res: RenderResolution)
RP.0 — set the logical render resolution. Rebuilds the scene-DDA
resources on the next Self::render_scene when the render size
changes.
Sourcepub fn set_ssaa(&mut self, factor: u8)
pub fn set_ssaa(&mut self, factor: u8)
RP.1 — set the supersampling factor (clamped to 1..=4). 1 = off.
Sourcepub fn set_posterize(&mut self, cfg: Option<PosterizeGpu>)
pub fn set_posterize(&mut self, cfg: Option<PosterizeGpu>)
RP.2 — set (or clear) the posterize post. Applied per-frame via the resolve uniform, so no pipeline rebuild is needed.
Sourcepub fn set_tint(&mut self, tint: Option<(u32, u8)>)
pub fn set_tint(&mut self, tint: Option<(u32, u8)>)
WT.2 — set (or clear) this frame’s full-screen tint: packed
0x00RRGGBB + strength quantized to 0..=255 (the blend is
integer — (c·(255−s₈) + t·s₈ + 127)/255 per channel, the
same expression the CPU backend runs, so tinted frames are
bit-exact across backends). Applied in the resolve pass at
the logical resolution, before the posterize quantize.
None — and a strength of 0, folded HERE so direct callers
can’t disable it — keeps the identity-resolve fast path
(byte-identical output, no full-screen pass). Per-frame state
— the facade forwards FrameParams::tint on every render.
Sourcepub fn logical_dims(&self) -> (u32, u32)
pub fn logical_dims(&self) -> (u32, u32)
RP.0 — the logical (retro) grid size the scene resolves to before the
upscale, resolved against the swapchain size. logical_dims == surface_dims under RenderResolution::Native.
Sourcepub fn render_dims(&self) -> (u32, u32)
pub fn render_dims(&self) -> (u32, u32)
RP.1 — the resolution the scene/sprite passes actually march at:
logical_dims × ssaa. The framebuffer + depth buffer are sized to this.
Sourcepub fn surface_dims(&self) -> (u32, u32)
pub fn surface_dims(&self) -> (u32, u32)
RP.0 — the swapchain (native window) size.
Sourcepub fn render(&mut self)
pub fn render(&mut self)
GPU.1 render: single render pass clearing the swapchain to a slowly drifting colour, then presenting. Voxels arrive in GPU.3+.
Sourcepub fn render_scene(
&mut self,
scene: &GpuSceneResident,
cameras: &[Camera],
grid_world: &[GridWorldTransform],
sprite_camera: &Camera,
fov_y_rad: f32,
max_outer_steps: u32,
)
pub fn render_scene( &mut self, scene: &GpuSceneResident, cameras: &[Camera], grid_world: &[GridWorldTransform], sprite_camera: &Camera, fov_y_rad: f32, max_outer_steps: u32, )
GPU.5 render — multi-grid scene marcher. cameras[i] is the
world camera transformed into grid i’s local frame
(caller-supplied; see scene-demo’s redraw_gpu for the
glam-based transform). fov_y_rad is the shared vertical
FOV; max_outer_steps caps per-ray chunk-DDA work for each
grid.
§Panics
If cameras.len() != scene.grid_count.
cameras[i] is grid i’s world camera transformed into that
grid’s local frame (the grid marcher works in grid-local space).
sprite_camera is the world camera: instanced sprites carry
world-space positions/transforms, so they must project through
the untransformed world camera — not cameras[0], which is only
the world camera when grid 0 is at identity.
Sourcepub fn render_clear_deferred(&mut self)
pub fn render_clear_deferred(&mut self)
Like Self::render (clear to colour) but deferred: stashes
the frame for Self::present / [Self::paint_egui] instead of
presenting. The facade uses this before any grid is resident so a
HUD can still be painted over an empty scene.
Sourcepub fn present(&mut self)
pub fn present(&mut self)
Present the frame stashed by the last deferred render
(Self::render_scene / Self::render_clear_deferred). No-op
if nothing is pending (e.g. the surface was lost mid-render).
Sourcepub fn wait_idle(&mut self)
pub fn wait_idle(&mut self)
Block until the GPU has drained every submitted command (queue
idle), dropping any not-yet-presented swapchain frame first. Call at
shutdown — before the GpuRenderer (and its window) drop — so the
device is torn down with no work in flight and no half-presented
frame, instead of yanking the swapchain mid-submission (which leaves
the driver/compositor compositing stale buffers — the “leftover
triangles / flicker after an unclean exit” symptom). No-op on wasm
(poll(Wait) is unavailable there; the browser reclaims the device).
Sourcepub fn project_point(
&self,
cam_pos: [f32; 3],
right: [f32; 3],
down: [f32; 3],
forward: [f32; 3],
world: [f32; 3],
) -> Option<(f32, f32)>
pub fn project_point( &self, cam_pos: [f32; 3], right: [f32; 3], down: [f32; 3], forward: [f32; 3], world: [f32; 3], ) -> Option<(f32, f32)>
Project a world point to window pixels under the marcher’s
vertical-FOV pinhole (the inverse of Self::pixel_ray), using
the last-rendered frame’s size + FOV. None before the first
scene render or for a point at/behind the near plane.
Sourcepub fn set_sprite_instances(
&mut self,
registry: &SpriteModelRegistry,
instances: &[SpriteInstance],
)
pub fn set_sprite_instances( &mut self, registry: &SpriteModelRegistry, instances: &[SpriteInstance], )
GPU.10.1 — upload a sprite model registry + its instances for the DDA path. An empty instance slice clears all sprites.
Sourcepub fn append_sprite_instances(
&mut self,
registry: &SpriteModelRegistry,
instances: &[SpriteInstance],
) -> u32
pub fn append_sprite_instances( &mut self, registry: &SpriteModelRegistry, instances: &[SpriteInstance], ) -> u32
Incrementally append sprite instances without rebuilding the
registry — the cheap streaming-spawn path (asteroids, projectiles).
Returns the index of the first appended instance ([base, base+N)).
Every appended instance must reference a model already registered
by the Self::set_sprite_instances that established residency
(model volumes are not re-uploaded here — build the full
SpriteModelRegistry up front and seed it once, then stream
instances). If no registry is resident yet, this performs the
initial full upload and returns 0.
Cost is amortised O(1) per instance (the GPU instance buffer grows
by powers of two), versus the full volume + buffer rebuild of
Self::set_sprite_instances.
Sourcepub fn remove_sprite_instance(&mut self, index: usize) -> Option<usize>
pub fn remove_sprite_instance(&mut self, index: usize) -> Option<usize>
Remove the sprite instance at index (swap-remove, O(1), no model
re-upload). Returns Some(old_last) if a different instance was
moved into index to fill the hole — its index changed from
old_last to index, so a caller tracking instance handles must
update that one. Returns None if index was the last element /
out of range, or no registry is resident.
Sourcepub fn flush_writes(&self)
pub fn flush_writes(&self)
Flush queued write_buffer uploads by submitting an empty command
stream. wgpu stages write_buffer data and flushes it on the next
Queue::submit; calling this between batches of uploads (e.g. a
flipbook’s frames in Self::add_sprite_model) recycles the device
staging pool so a big one-shot batch can’t exhaust it (which would
then crash egui-wgpu’s own write_buffer).
Sourcepub fn add_sprite_model(
&mut self,
registry: &SpriteModelRegistry,
chain_id: u32,
)
pub fn add_sprite_model( &mut self, registry: &SpriteModelRegistry, chain_id: u32, )
Incrementally add model chain_id (its full LOD chain) from
registry to the resident sprite registry without
re-uploading the existing models — the streaming-in counterpart
to Self::append_sprite_instances. Register the model on the
CPU registry first (add / add_lod), then pass the returned
chain_id here; afterwards instances may reference it.
If no registry is resident yet, this instead performs the
initial full upload of registry (all current models, zero
instances) to establish residency. Cost is amortised O(new
model voxels): the shared volume buffers carry slack and
bump-append, rebuilding from the registry only on overflow.
Sourcepub fn remove_sprite_model(&mut self, chain_id: u32)
pub fn remove_sprite_model(&mut self, chain_id: u32)
Remove a model (tombstone its LOD chain) from the resident sprite
registry — the counterpart to Self::add_sprite_model. Frees its
colors/dirs space for reuse by a later add; the smaller
occupancy/color_offsets holes are reclaimed by
Self::compact_sprite_models. Entry / chain ids stay stable, so
other models’ chain_ids remain valid.
Instances of the removed model keep their slots but draw as nothing
until the caller drops them via Self::remove_sprite_instance.
No-op if chain_id is unknown / already removed / no registry.
Sourcepub fn compact_sprite_models(&mut self, registry: &SpriteModelRegistry)
pub fn compact_sprite_models(&mut self, registry: &SpriteModelRegistry)
Reclaim the holes left by Self::remove_sprite_model by rebuilding
the shared volume buffers from the live models only. registry must
be the resident one. Cost is O(live volume) — call it when
Self::dead_sprite_model_count is high (e.g. exceeds the live
count), not every frame. No-op if no registry is resident.
Sourcepub fn sprite_model_count(&self) -> usize
pub fn sprite_model_count(&self) -> usize
Number of live (non-removed) sprite models (0 if none uploaded).
Sourcepub fn dead_sprite_model_count(&self) -> usize
pub fn dead_sprite_model_count(&self) -> usize
Number of removed-but-not-yet-compacted sprite models — the
fragmentation signal for deciding when to call
Self::compact_sprite_models.
Sourcepub fn sprite_instance_count(&self) -> usize
pub fn sprite_instance_count(&self) -> usize
Number of resident sprite instances (0 if none uploaded).
Sourcepub fn update_sprite_instance_transforms(
&mut self,
instances: &[SpriteInstance],
)
pub fn update_sprite_instance_transforms( &mut self, instances: &[SpriteInstance], )
Re-pose the already-resident sprite instances in place (no model
volume re-upload) — the cheap per-frame path for animated KFA
limbs. instances must match the last Self::set_sprite_instances
in length + order. No-op if no sprite registry is resident.
Sourcepub fn update_sprite_model(
&mut self,
registry: &SpriteModelRegistry,
chain_id: u32,
)
pub fn update_sprite_model( &mut self, registry: &SpriteModelRegistry, chain_id: u32, )
GPU.12 incremental — re-upload only LOD chain chain_id’s entries
after an in-place edit of registry (carve / recolour), without
rebuilding the whole sprite registry. registry must be the one
last passed to Self::set_sprite_instances with chain
chain_id already edited. No-op if no registry is resident.
Sourcepub fn set_sprite_instance_model(
&mut self,
registry: &SpriteModelRegistry,
index: usize,
chain_id: u32,
)
pub fn set_sprite_instance_model( &mut self, registry: &SpriteModelRegistry, index: usize, chain_id: u32, )
VCL.2 — repoint sprite instance index at LOD chain chain_id
(the per-frame flipbook step for animated voxel clips). registry
is the resident one; chain_id’s volume must already be uploaded
(e.g. a clip’s frames registered via Self::add_sprite_model).
CPU-side rewrite picked up by the next frame’s cull — no volume
re-upload. No-op if no registry is resident.
Sourcepub fn set_sprite_instance_colmul(&mut self, tables: &[[u64; 256]])
pub fn set_sprite_instance_colmul(&mut self, tables: &[[u64; 256]])
Set the per-instance kv6colmul[256] lighting tables (voxlap’s
update_reflects output, e.g. via roxlap_core::sprite:: sprite_colmul), in the same order/length as the last
Self::set_sprite_instances. The GPU sprite pass modulates each
voxel by its surface normal’s entry — matching the CPU rasteriser.
No-op if no sprite registry is resident.
Sourcepub fn set_sprite_lod_px(&mut self, px: f32)
pub fn set_sprite_lod_px(&mut self, px: f32)
GPU.10.4 — set the LOD pixel threshold: a sprite steps to the
next mip once a mip-0 voxel would project below px screen
pixels. 1.0 is the natural “no sub-pixel voxels” default;
larger values force LOD in closer (useful for inspection).
Clamped to ≥ 0.25.
Sourcepub fn set_scene_mip_scan_dist(&mut self, dist: f32)
pub fn set_scene_mip_scan_dist(&mut self, dist: f32)
GPU.11.1 — set the scene-grid LOD scan distance (world units).
A chunk entered at world-t t is marched at mip
floor(log2(max(t, msd) / msd)), clamped to its grid’s mip
ladder. 0 disables LOD (always mip-0). Larger values push
the coarser mips farther out — the axis-aligned-mip-beams
mitigation lever (GPU.11.2). Default 64 (matches CPU
mip_scan_dist).
Sourcepub fn set_scene_side_shades(&mut self, s: [i8; 6])
pub fn set_scene_side_shades(&mut self, s: [i8; 6])
Set per-face grid side-shading — voxlap’s
setsideshades(top, bot, left, right, up, down). Each value is
subtracted (as a u8, matching the CPU gcsub high byte) from a
hit voxel’s brightness byte before shading, so the scene-DDA pass
darkens grid faces the same way the CPU rasteriser does. [0; 6]
disables it (the default). The hit face is taken from the DDA’s
last-stepped axis + ray direction.
Sourcepub fn set_sprite_materials(&mut self, table: &MaterialTable)
pub fn set_sprite_materials(&mut self, table: &MaterialTable)
TV — set the global voxel-material palette for the GPU sprite pass.
Mirrors the renderer’s MaterialTable:
every sprite/clip instance’s material id indexes it for opacity +
blend mode. Cheap (2 KB); call it whenever the palette changes (or
each frame). While every material is opaque the shader stays on the
unchanged first-hit path.
Sourcepub fn set_scene_terrain_materials(
&mut self,
table: &MaterialTable,
map: &[(Rgb, u8)],
)
pub fn set_scene_terrain_materials( &mut self, table: &MaterialTable, map: &[(Rgb, u8)], )
TV.6 — set the scene (terrain) material palette + colour→material map for the multi-grid scene pass. Matching-colour terrain voxels render translucent (and/or emissive, EV.2); an empty map / all-opaque non-emissive palette renders unchanged. The map is capped at 256 rows (the fixed buffer size).