simvx.graphics.renderer.forward

Default renderer: Vulkan forward path; implements the RendererBackend ABC.

Module Contents

Classes

Renderer

Default Vulkan forward renderer: multi-draw indirect, per-viewport frustum culling.

Data

API

simvx.graphics.renderer.forward.__all__

[‘Renderer’]

simvx.graphics.renderer.forward.log

‘getLogger(…)’

class simvx.graphics.renderer.forward.Renderer(engine: Any, max_objects: int = 10000)[source]

Bases: simvx.graphics.renderer._base.RendererBackend

Default Vulkan forward renderer: multi-draw indirect, per-viewport frustum culling.

Initialization

property draw2d_draw_count: int[source]

The 2D draw count of the last main-framebuffer frame (telemetry).

The item pipeline submits through the bindless co-batched pass, so its last_frame_draw_count is the live count; 0 before the pass is first created (a scene that drew no 2D yet).

set_item_view(view: Any, camera: tuple) None[source]

Bind a published 2D item view + camera affine for this frame’s submit.

The synchronous-path entry point: the frame loop calls this after publishing the item view, and render submits it through the bindless co-batched submitter. view=None means no 2D this frame. The pipelined path binds the same fields via install_packet from the packet’s item_view.

setup() None[source]

Initialize GPU resources: called once after engine Vulkan init.

set_skybox(cubemap: simvx.graphics.engine.CubemapHandle) None[source]

Set a cubemap as the skybox and run the IBL precompute.

Accepts a :class:~simvx.graphics.engine.CubemapHandle returned by

Meth:

Engine.load_cubemap. The renderer takes ownership of the underlying Vulkan resources and destroys them at shutdown. IBL precompute (irradiance + prefiltered specular + BRDF LUT) is run once per unique handle and cached, mirroring the web renderer’s IBLPass._byCubemapId lazy-precompute behaviour. Re-setting the same handle is a no-op past the cache check.

property post_processing: simvx.graphics.renderer.post_process.PostProcessPass | None[source]

Access post-processing pass for configuration.

property custom_post_processing: simvx.graphics.renderer.custom_post_process.CustomPostProcessPass | None[source]

Access custom user post-process pass for configuration.

set_gizmo_data(data: simvx.graphics.renderer.gizmo_pass.GizmoRenderData | None) None[source]

Set gizmo render data for the current frame (or None to hide).

property ssao: simvx.graphics.renderer.ssao_pass.SSAOPass | None[source]

Access SSAO pass for configuration.

set_materials(materials: numpy.ndarray) None[source]

Set material array and upload to GPU (skips if unchanged).

set_lights(lights: numpy.ndarray) None[source]

Set light array and upload to GPU (skips if unchanged).

Prepends uint32 light_count to match GLSL LightBuffer layout: [uint32 count][Light[0]][Light[1]]…

capture_reflection_probes(cmd: Any, adapter: Any) bool[source]

Capture any new / requested reflection probes. Returns True if any captured.

Driven from the app’s pre_render hook (before the main render pass) so a capture this frame is visible the same frame. Probe faces, the per-probe IBL convolution, and the cube-array copy all record into the SAME primary frame command buffer cmd: there is no separate submit/wait, so an always-mode probe no longer stalls the queue every frame. Probe face rendering reuses the offscreen scene path and clobbers per-frame submission lists (which render_to_target saves/restores).

capture_irradiance_volumes(cmd: Any, adapter: Any) bool[source]

Bake budgeted irradiance-volume probes into the b18 SH SSBO.

Driven from pre_render right after :meth:capture_reflection_probes, into the same primary cmd: the SH-reduce compute writes this frame’s probes and a buffer barrier makes them visible to the main pass’s uber reads, so a baked probe lights the scene the same frame. Flips _irradiance_volume_enabled (the uber SH gate) on when a volume is present; when none exist the flag stays off and the frame is byte-identical to a scene with no volume.

init(device: Any, swapchain: Any) None[source]

Initialize (called by ABC contract: use setup() instead).

begin_frame() Any[source]

Begin frame: grow the transform arena if scheduled, then clear submission lists.

submit_instance(mesh_handle: simvx.graphics.types.MeshHandle, transform: numpy.ndarray, material_id: int = 0, viewport_id: int = 0, render_layers: int = 0) None[source]

Submit a mesh instance for rendering this frame.

render_layers is the instance’s render-layer bitmask, uploaded into the Transform SSBO so the decal uber can enforce Decal3D.cull_mask. 0 means “all layers” (every decal received).

submit_multimesh(mesh_handle: simvx.graphics.types.MeshHandle, transforms: numpy.ndarray, material_id: int = 0, material_ids: numpy.ndarray | None = None, viewport_id: int = 0, count: int = 0, cache_key: int = 0, version: int = 0) None[source]

Bulk-submit many instances of the same mesh: avoids per-instance Python loops.

Args: mesh_handle: Shared mesh for all instances. transforms: (N, 4, 4) float32 array of model matrices. material_id: Material index for all instances (ignored if material_ids given). material_ids: Optional (N,) uint32 array of per-instance material indices. viewport_id: Viewport index. count: Ignored here (transforms already sliced); accepted for ABI parity with web. cache_key / version: stable per-node id + content generation; let _upload_multimesh_blocks skip re-uploading an unchanged static block.

submit_shader_instance(mesh_handle: simvx.graphics.types.MeshHandle, transform: numpy.ndarray, material_id: int, shader_material: Any) None[source]

Submit a MeshInstance3D that carries a ShaderMaterial.

The per-material pipeline is lazy-compiled + cached on first use via ShaderMaterialManager. During the forward draw, the renderer splits these submissions into their own bucket, binds the custom pipeline, updates the uniform buffer, and draws one mesh at a time, per- material pipeline switching is the cost of the custom-shader path.

submit_particles(particle_data: numpy.ndarray) None[source]

Submit particle data for rendering this frame.

msdf_atlas_slot() int[source]

Ensure the MSDF atlas is uploaded + bound in the bindless array; return its slot.

Used by the Billboard2DPass collector (scene_adapter) to bake the atlas slot into Text3D glyph rows. Must be called OUTSIDE any render pass (it does staging transfers + vkUpdateDescriptorSets via register_texture / update_texture); submit_scene is such a point. Returns -1 when there is no text pass or no atlas yet.

The slot is reused across frames; if the atlas view is rebuilt (font / glyph-set change), the same slot is rewritten so baked references stay valid (assign-once, like the bindless-pass atlas slot).

submit_billboards(billboards: numpy.ndarray) None[source]

Submit depth-tested billboard instances (Sprite3D / Text3D) for this frame.

billboards is a BILLBOARD_DTYPE array (one row per Sprite3D quad or Text3D glyph). All submissions are concatenated and drawn in one call by

Class:

Billboard2DPass inside render_scene_content.

draw_sprite_3d(*, texture_id: int, position: Any, width: float, height: float, uv: tuple[float, float, float, float], colour: Any = (1.0, 1.0, 1.0, 1.0), billboard: bool = True, centered: bool = True, offset: tuple[float, float] = (0.0, 0.0), alpha_cut: str = 'disabled', render_priority: int = 0) None[source]

Emit one Sprite3D billboard quad (called by Sprite3D.on_draw).

Builds a single BILLBOARD_DTYPE row: the quad is anchored at position and sized width x height in world units on the camera-facing plane. centered keeps the quad on the anchor; otherwise its top-left corner sits on the anchor. The camera basis is applied GPU-side by Billboard2DPass.

draw_text_3d(*, text: str, position: Any, font_scale: float = 1.0, pixel_size: float = 0.01, colour: Any = (1.0, 1.0, 1.0, 1.0), alignment: str = 'centre', offset: tuple[float, float] = (0.0, 0.0)) None[source]

Emit a Text3D MSDF glyph billboard run (called by Text3D.on_draw).

Reuses the unified glyph layout (:func:layout_glyph_run) so kerning / size match every other text node, then maps the pixel-space glyph quads into world units (pixel_size) on the camera-facing plane, centred about the anchor, and emits one MSDF billboard row per glyph (is_msdf set so Billboard2DPass decodes glyph AA). The MSDF atlas must already be bound bindless (the collector ensured the slot outside the render pass).

submit_gpu_particles(emitter_config: dict, *, emitter_id: int = 0) None[source]

Submit a GPU particle emitter config for compute-shader simulation this frame.

emitter_id is a stable per-node identifier (id(node) & 0xFFFFFFFF from :class:SceneAdapter). :class:ParticleCompute keys its persistent SSBOs by this id so multi-emitter scenes render correctly: matches the web GPUParticlePass ownership model.

submit_light2d(**kwargs) None[source]

Submit a 2D light for this frame (forwarded to Light2DPass).

submit_occluder2d(polygon_vertices: list[tuple[float, float]]) None[source]

Submit a 2D occluder polygon for shadow casting this frame.

submit_skinned_instance(mesh_handle: simvx.graphics.types.MeshHandle, transform: numpy.ndarray, material_id: int, joint_matrices: numpy.ndarray) None[source]

Submit a skinned mesh instance with joint matrices for this frame.

install_packet(packet: Any) None[source]

Bind a :class:RenderPacket’s owned snapshot onto the per-frame state.

Called on the RENDER THREAD (under _frame_state_lock) immediately before recording a pipelined GPU frame. It rebinds the renderer’s per-frame submission attributes to the packet’s OWNED copies and rebuilds viewport_manager from the packet’s viewport snapshots, so the existing _upload_transforms / pre_render / render body records from the snapshot instead of from live state the main thread is concurrently rebuilding. This is the same temporary-rebind pattern render_to_target uses for offscreen SRUs (scene_adapter ~1046), here for the whole frame.

The renderer’s own set_materials / set_lights still own the GPU material/light SSBO uploads; this only restores the CPU-side arrays the recording reads, so passes that re-read _materials / _lights see the snapshot. (Those SSBOs are written by pre_render/_upload on this same render thread, so a single GPU buffer remains correct.)

tilemap_layers() list[source]

Tilemap submissions the render pass should draw this frame.

Returns the packet’s owned snapshot when a packet is installed (pipelined render thread), else the live _tilemap_pass._submissions list (synchronous path, byte-identical). [] when there is no tilemap pass.

light2d_submissions() tuple[list, list][source]

(lights, occluders) the 2D-light pass should render this frame.

Packet-owned snapshots in pipelined mode, else the live _light2d_pass lists (synchronous path, byte-identical). ([], []) when there is no 2D-light pass.

reserve_main_slice() int[source]

Reserve the main scene’s transform-SSBO slice at the front of the frame.

Called before any offscreen scene-render unit (SubViewport / probe face) reserves, so the main scene is the FIRST SRU and keeps base 0: its absolute first_instance indices equal the local indices (unchanged single-scene behaviour). Offscreen SRUs then reserve slices after it. Idempotent within a frame (the arena cursor only advances on the first call). Returns the base.

sync_render_state() None[source]

Propagate WorldEnvironment state and run feature activations.

Split out of :meth:pre_render so the frame loop can run it BEFORE it records offscreen RenderView / SubViewport draws. The G-buffer / SSR / SSGI activation inside sync_world_environment may vkDeviceWaitIdle and rebuild the HDR-pass pipelines; doing that after an offscreen target has already recorded pipeline binds into the open command buffer would invalidate that buffer (a hard crash when SSR is combined with a live PlanarReflection3D). This records nothing into a command buffer, so hoisting it is safe.

Guarded to run at most once per frame (reset in :meth:begin_frame): the earliest caller performs the sync, and the later :meth:pre_render call is a no-op, so there is exactly one activation transition per frame and it always precedes any offscreen pipeline binding.

pre_render(cmd: Any) None[source]

Record offscreen passes (shadow maps, HDR) before main render pass begins.

render(cmd: Any) None[source]

Record draw commands for all viewports.

apply_gbuffer_state(active: bool) None[source]

Activate/deactivate the thin G-buffer.

Flipping it changes the HDR pass’s colour-attachment count, so the HDR target and every pipeline drawing into it must be rebuilt. This is rare (a consumer toggling on/off) and idempotent: a no-op when the state already matches. Requires the post-process HDR pipeline (the G-buffer rides its second attachment); ignored until it exists.

Mirrors the env_sync device-wait-idle + rebuild idiom: it only runs on a transition, never per frame.

apply_ssr_state(active: bool, *, intensity: float = 1.0, max_distance: float = 40.0, roughness_cutoff: float = 0.6) None[source]

Activate/deactivate screen-space reflections.

SSR requires the post-process HDR path AND the thin G-buffer (env-sync turns the G-buffer on for SSR before calling this). When it cannot run, or is disabled, b16 falls back to the placeholder and the uber’s indirect_specular_enabled gate goes 0: byte-identical to feature-off. Idempotent: the descriptor rebind + flag flip only happen on a transition.

apply_ssgi_state(active: bool, *, intensity: float = 1.0, max_distance: float = 8.0) None[source]

Activate/deactivate screen-space global illumination.

The diffuse mirror of :meth:apply_ssr_state. SSGI requires the post-process HDR path AND the thin G-buffer (env-sync turns the G-buffer on for SSGI before calling this). When it cannot run, or is disabled, b17 falls back to the placeholder and the uber’s indirect_diffuse_enabled gate goes 0: byte-identical to feature-off. Idempotent: the descriptor rebind + flag flip only happen on a transition.

apply_probe_quality() None[source]

Apply the WorldEnvironment reflection-probe quality dials.

_probe_blend_count / _probe_face_size are written by the env-sync spec each frame; None keeps the built-in defaults (top-2 blend, 128 px faces), i.e. the exact pre-knob paths. Both downstream setters are transition-gated no-ops when the effective value is unchanged, so this per-frame call is free in steady state. A blend count of 0 leaves the shader permutation alone: the capture gate in

Meth:

capture_reflection_probes keeps the probe set empty, which makes the blend loop a branch-free no-op.

property render_scale: float[source]

Resolution scale of the main-view HDR chain.

internal_extent = ceil(output_extent * render_scale) sizes the whole HDR chain (colour + depth, SSAO, bloom inputs, volumetric fog, velocity); the tonemap fullscreen draw bilinearly upscales to the swapchain. With TAA enabled the resolve upgrades that upscale to TAAU: its output + history live at the OUTPUT extent and accumulate sub-pixel detail from the jittered internal-res frames. 1.0 (default) is byte-identical to the unscaled path. Driven by WorldEnvironment.render_scale via the env-sync spec; an actual change drains the device and rebuilds the chain (the same rare transition as a window resize), so per-frame re-assignment of the same value is free.

internal_extent(width: int | None = None, height: int | None = None) tuple[int, int][source]

HDR-chain render extent: ceil(output * render_scale), min 1 px.

Defaults to the engine’s current output extent. Identity when render_scale is 1.0 or post-processing is off (no HDR chain means no upscale point, so the scale is inert and shaders must not be told otherwise).

resize(width: int, height: int) None[source]

Handle framebuffer resize: recreate post-process targets + 3D pipelines.

width / height are the OUTPUT (swapchain) extent; the HDR chain and every pass that renders in HDR space is (re)created at the internal extent. Identical to the output extent at render_scale 1.0.

cleanup() None[source]

Release all GPU resources.

destroy() None[source]

ABC destroy: delegates to cleanup.

register_mesh(streams: simvx.graphics.types.VertexStreams, indices: numpy.ndarray) simvx.graphics.types.MeshHandle[source]

Register a mesh’s vertex streams on GPU via engine’s mesh registry.

upload_texture_pixels(pixels: numpy.ndarray, width: int, height: int) int[source]

Upload RGBA pixel data to GPU, return bindless texture index.

capture_frame() numpy.ndarray[source]

Capture the last rendered frame as (H, W, 4) uint8 RGBA numpy array.

__slots__

()