simvx.graphics.renderer.forward¶
Default renderer: Vulkan forward path; implements the RendererBackend ABC.
Module Contents¶
Classes¶
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)¶
Bases:
simvx.graphics.renderer._base.RendererBackendDefault Vulkan forward renderer: multi-draw indirect, per-viewport frustum culling.
Initialization
- property draw2d_draw_count: int¶
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_countis the live count;0before the pass is first created (a scene that drew no 2D yet).
- set_item_view(view: Any, camera: tuple) None¶
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
rendersubmits it through the bindless co-batched submitter.view=Nonemeans no 2D this frame. The pipelined path binds the same fields viainstall_packetfrom the packet’sitem_view.
- setup() None¶
Initialize GPU resources: called once after engine Vulkan init.
- set_skybox(cubemap: simvx.graphics.engine.CubemapHandle) None¶
Set a cubemap as the skybox and run the IBL precompute.
Accepts a :class:
~simvx.graphics.engine.CubemapHandlereturned 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’sIBLPass._byCubemapIdlazy-precompute behaviour. Re-setting the same handle is a no-op past the cache check.
- rebake_skybox_ibl() bool¶
Re-run the IBL convolution over the installed cubemap’s new texels.
For a caller that has rewritten the cube in place with
- Meth:
Engine.update_cubemap_faces. Nothing about the installation changed – the image, the view, the sampler, theSkyboxPassand its pipeline are all still the right ones – so re-installing would tear all of that down and build it again to arrive back where it started. Only the ambient bake actually depends on the texels.
This is what makes an animated sky affordable.
set_skyboxis keyed on handle identity, so a rewritten cube under a retained handle would otherwise never re-bake and the ambient light would freeze at the first frame’s colours.Returns
Falsewhen there is nothing installed to re-bake.
- property post_processing: simvx.graphics.renderer.post_process.PostProcessPass | None¶
Access post-processing pass for configuration.
- property custom_post_processing: simvx.graphics.renderer.custom_post_process.CustomPostProcessPass | None¶
Access custom user post-process pass for configuration.
- set_gizmo_data(data: simvx.graphics.renderer.gizmo_pass.GizmoRenderData | None) None¶
Set gizmo render data for the current frame (or None to hide).
- property ssao: simvx.graphics.renderer.ssao_pass.SSAOPass | None¶
Access SSAO pass for configuration.
- set_materials(materials: numpy.ndarray) None¶
Set the material array and upload it to this frame’s ring slot.
- property shadow_caster_budget: int¶
Positional shadow casters of each kind the renderer will honour.
WorldEnvironment.shadow_caster_countwhen the scene sets it, else the engine default, bounded by the caster slots the shadow SSBO addresses AND by the rows the device will allocate an atlas for.
- set_lights(lights: numpy.ndarray) None¶
Set the light array and upload it to this frame’s ring slot.
Prepends uint32 light_count to match GLSL LightBuffer layout: [uint32 count][Light[0]][Light[1]]…
Positional shadow casters are given their atlas row here, before the upload, because the row travels to the shader in
Light.params.w.
- capture_reflection_probes(cmd: Any, adapter: Any) bool¶
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¶
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¶
Initialize (called by ABC contract: use setup() instead).
- begin_frame() Any¶
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¶
Submit a mesh instance for rendering this frame.
render_layersis the instance’s render-layer bitmask, uploaded into the Transform SSBO so the decal uber can enforceDecal3D.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¶
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_blocksskip re-uploading an unchanged static block.
- submit_shader_instance(mesh_handle: simvx.graphics.types.MeshHandle, transform: numpy.ndarray, material_id: int, shader_material: Any, texture_slots: dict[str, int] | None = None) None¶
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.
texture_slotsmaps each texture name the material bound to the backend handle the scene adapter resolved it to, so the manager writes the material’s descriptor group without loading anything itself.
- submit_particles(particle_data: numpy.ndarray) None¶
Submit particle data for rendering this frame.
- msdf_atlas_slot() int¶
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 +
vkUpdateDescriptorSetsviaregister_texture/update_texture);submit_sceneis 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¶
Submit depth-tested billboard instances (Sprite3D / Text3D) for this frame.
billboardsis aBILLBOARD_DTYPEarray (one row per Sprite3D quad or Text3D glyph). All submissions are concatenated and drawn in one call by- Class:
Billboard2DPassinsiderender_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¶
Emit one Sprite3D billboard quad (called by
Sprite3D.on_draw).Builds a single
BILLBOARD_DTYPErow: the quad is anchored atpositionand sizedwidthxheightin world units on the camera-facing plane.centeredkeeps 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¶
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_msdfset 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¶
Submit a GPU particle emitter config for compute-shader simulation this frame.
emitter_idis a stable per-node identifier (id(node) & 0xFFFFFFFFfrom :class:SceneAdapter). :class:ParticleComputekeys its persistent SSBOs by this id so multi-emitter scenes render correctly: matches the webGPUParticlePassownership model.
- submit_light2d(**kwargs) None¶
Submit a 2D light for this frame (forwarded to Light2DPass).
- submit_occluder2d(polygon_vertices: list[tuple[float, float]]) None¶
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¶
Submit a skinned mesh instance with joint matrices for this frame.
- install_packet(packet: Any) None¶
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 rebuildsviewport_managerfrom the packet’s viewport snapshots, so the existing_upload_transforms/pre_render/renderbody records from the snapshot instead of from live state the main thread is concurrently rebuilding. This is the same temporary-rebind patternrender_to_targetuses for offscreen SRUs (scene_adapter ~1046), here for the whole frame.The renderer’s own
set_materials/set_lightsstill own the GPU material/light SSBO uploads; this only restores the CPU-side arrays the recording reads, so passes that re-read_materials/_lightssee the snapshot. (Those SSBOs are written bypre_render/_uploadon this same render thread, so a single GPU buffer remains correct.)
- tilemap_layers() list¶
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._submissionslist (synchronous path, byte-identical).[]when there is no tilemap pass.
- light2d_submissions() tuple[list, list]¶
(lights, occluders)the 2D-light pass should render this frame.Packet-owned snapshots in pipelined mode, else the live
_light2d_passlists (synchronous path, byte-identical).([], [])when there is no 2D-light pass.
- reserve_main_slice() int¶
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_instanceindices 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.
- property tiled_lighting_enabled: bool¶
Whether a many-light frame may cull its lights into screen tiles.
Off by default. When on, a frame carrying at least
- Data:
~simvx.graphics.renderer.light_cull_pass.TILED_LIGHTING_MIN_LIGHTSlights runs a compute pass that assigns each light to the 16x16 pixel tiles it can reach, and the fragment shader loops over its own tile’s list instead of every light in the scene. Below the threshold, and on any frame that also records an offscreen scene-render unit, the frame takes the brute-force loop unchanged.
- sync_render_state() None¶
Propagate
WorldEnvironmentstate and run feature activations.Split out of :meth:
pre_renderso the frame loop can run it BEFORE it records offscreen RenderView / SubViewport draws. The G-buffer / SSR / SSGI activation insidesync_world_environmentmayvkDeviceWaitIdleand 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 livePlanarReflection3D). 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_rendercall 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¶
Record offscreen passes (shadow maps, HDR) before main render pass begins.
- render(cmd: Any) None¶
Record draw commands for all viewports.
- apply_gbuffer_state(active: bool) None¶
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_syncdevice-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¶
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_enabledgate 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¶
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’sindirect_diffuse_enabledgate goes 0: byte-identical to feature-off. Idempotent: the descriptor rebind + flag flip only happen on a transition.
- apply_probe_quality() None¶
Apply the WorldEnvironment reflection-probe quality dials.
_probe_blend_count/_probe_face_sizeare written by the env-sync spec each frame;Nonekeeps 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_probeskeeps the probe set empty, which makes the blend loop a branch-free no-op.
- apply_shadow_quality() None¶
Apply the WorldEnvironment positional shadow caster budget.
_shadow_caster_countis written by the env-sync spec each frame;Nonekeeps the built-in default of one caster of each kind, which holds the atlases at one row. It is not a return to the pre-budget image: with more than one positional light the frame differs, because each light is now darkened by its own map alone instead of by the single caster’s. The atlas resize is a transition-gated no-op when the effective value is unchanged, so this per-frame call is free in steady state. A budget of 0 leaves the atlases alone: no light is handed a caster slot, which is what switches shadows off.No shader permutation depends on the count – the atlas row divide reads it out of the shadow SSBO – but a real change still drains the device to reallocate the atlases, so it must land before the frame records anything: this is called from
sync_world_environment, which the frame loop runs through :meth:sync_render_stateahead of every offscreen and main-pass recording, for the same reason the G-buffer activation is.
- property render_scale: float¶
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 byWorldEnvironment.render_scalevia 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]¶
HDR-chain render extent:
ceil(output * render_scale), min 1 px.Defaults to the engine’s current output extent. Identity when
render_scaleis 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¶
Handle framebuffer resize: recreate post-process targets + 3D pipelines.
width/heightare 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¶
Release all GPU resources.
- destroy() None¶
ABC destroy: delegates to cleanup.
- register_mesh(streams: simvx.graphics.types.VertexStreams, indices: numpy.ndarray) simvx.graphics.types.MeshHandle¶
Register a mesh’s vertex streams on GPU via engine’s mesh registry.
- upload_texture_pixels(pixels: numpy.ndarray, width: int, height: int) int¶
Upload RGBA pixel data to GPU, return bindless texture index.
- capture_frame() numpy.ndarray¶
Capture the last rendered frame as (H, W, 4) uint8 RGBA numpy array.
- __slots__¶
()