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)[source]¶
Bases:
simvx.graphics.renderer._base.RendererBackendDefault 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_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[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
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.
- 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.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.
- 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_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[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_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) 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 +
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[source]¶
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[source]¶
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[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_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[source]¶
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_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 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[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._submissionslist (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_passlists (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_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.
- sync_render_state() None[source]¶
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[source]¶
Record offscreen passes (shadow maps, HDR) before main render pass begins.
- 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_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[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_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[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’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[source]¶
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.
- 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 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][source]¶
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[source]¶
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.
- 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__¶
()