simvx.graphics.gpu.secondary_engine¶
Secondary-engine facade for the D8 explicit-multi-adapter (multi-GPU) renderer.
The default :class:~simvx.graphics.renderer.forward.Renderer and every resource
manager it owns (PipelineManager / BufferManager / PassOrchestrator /
SceneContentRenderer / mesh_registry / the texture system) read their GPU
handles off one engine object: engine.ctx.device, engine.mesh_registry,
engine.texture_manager and so on. A VkPipeline / VkBuffer / descriptor
created on one VkDevice cannot be used on another, so to render an offloaded
SubViewport SRU on a secondary GPU we need a second, fully independent renderer
whose resources live on the secondary device.
Option A (the one implemented here) is to duplicate the renderer via an engine
facade: rather than refactor the renderer to take a device, we give it a
lightweight object that quacks exactly like the bits of Engine the renderer
reads, but backed by a secondary :class:~.multi_device.DeviceSlot. Then
Renderer(facade) + facade.attach_renderer() + renderer.setup() builds
all GPU resources on the secondary device, reusing every line of existing
renderer code unchanged. Nothing on the single-GPU path constructs a facade, so
that path is byte-identical to today.
Engine attribute surface the renderer + its managers + the passes + scene_adapter
read off the engine object (grep-derived from forward.py /
pipeline_manager.py / buffer_manager.py / pass_orchestrator.py /
scene_renderer.py / mesh_registry.py / scene_adapter.py):
========================== ================================================
attribute how the facade provides it
========================== ================================================
ctx a secondary :class:~.context.GPUContext (device +
queues + a CommandContext on the secondary device)
ctx.device secondary VkDevice (the single most-read handle)
ctx.physical_device secondary VkPhysicalDevice
ctx.graphics_queue secondary graphics queue
ctx.command_pool secondary command pool (via cmd_ctx)
mesh_registry per-device :class:~..renderer.mesh_registry.MeshRegistry
texture_manager per-device :class:~..materials.texture.TextureManager
(the facade itself is the registrar)
capabilities the shared probed snapshot (read-only facts)
_has_mdi mirrored from capabilities
shader_dir shared SHADER_DIR (SPIR-V is device-independent)
content_scale mirrored from the primary engine (SRU sizing)
extent the offscreen SRU size (no swapchain on a secondary)
render_pass a secondary offscreen-compatible VkRenderPass
push_constants records vkCmdPushConstants (device-independent)
register_texture / bindless texture registrar methods, recorded on the
upload_texture_pixels / secondary device’s descriptor set
update_texture /
unregister_texture
texture_descriptor_set secondary bindless descriptor set
texture_descriptor_layout secondary bindless descriptor layout
current_timestamp_pool None (per-pass GPU timing is optional)
renderer the secondary :class:Renderer once attached
capture_frame raises (a secondary never owns the swapchain)
_window / _sync None (a secondary never presents)
========================== ================================================
Honest scope: this module builds the facade + per-device residency helpers and is
covered by GPU-free unit tests asserting the full attribute surface. The actual
Renderer(facade).setup() GPU build + the offload-record-and-composite loop run
on the 4x Arc Pro B70 rig; they cannot be functionally verified on this single-GPU
box. Textured-SRU residency mirrors the bindless uploads the SRU samples; a
no-texture / vertex-colour SRU is the minimal first rig case and needs no texture
mirroring (see :class:SecondaryResidency).
Module Contents¶
Classes¶
Engine-like facade backed by a secondary :class: |
|
Mirrors to a secondary device the meshes + textures an offloaded SRU needs. |
Data¶
API¶
- simvx.graphics.gpu.secondary_engine.log¶
‘getLogger(…)’
- simvx.graphics.gpu.secondary_engine.__all__¶
[‘SecondaryRenderContext’, ‘SecondaryResidency’]
- class simvx.graphics.gpu.secondary_engine.SecondaryRenderContext(slot: simvx.graphics.gpu.multi_device.DeviceSlot, *, capabilities: simvx.graphics.gpu.capabilities.RenderCapabilities | None, content_scale: tuple[float, float] = (1.0, 1.0), extent: tuple[int, int] | None = None, render_pass: Any = None, max_textures: int = 4096)[source]¶
Engine-like facade backed by a secondary :class:
DeviceSlot.Exposes exactly the attribute surface the renderer + its managers + the passes
scene_adapter read off
Engine(see the module docstring table), but every GPU handle is the secondary device’s.Renderer(facade)therefore builds all its pipelines / buffers / descriptors on the secondary device, reusing the existing renderer code unchanged.
Construction does not touch the primary device: it only reads scalar facts (
content_scale,capabilities) from the primary engine and creates aCommandContext+ a :class:GPUContexton the secondary slot’s device. The expensive GPU build (pipelines, descriptor pools, SSBOs) happens later inRenderer(facade).setup(), on the rig.Initialization
- property ctx: simvx.graphics.gpu.context.GPUContext[source]¶
- property capabilities: simvx.graphics.gpu.capabilities.RenderCapabilities | None[source]¶
- property mesh_registry: simvx.graphics.renderer.mesh_registry.MeshRegistry[source]¶
Per-device mesh registry (lazy), mirroring
Engine.mesh_registry.
- property texture_manager: Any[source]¶
Per-device bindless texture manager (lazy), mirroring
Engine.texture_manager.The facade is its own registrar (it implements
register_texture/upload_texture_pixelson the secondary device), so the manager cachesresolves against secondary-device bindless slots.
- property renderer: Any[source]¶
The secondary :class:
Rendereronce bound via :meth:attach_renderer.Unlike
Engine.rendererthis does NOT lazily create a renderer: the offload coordinator constructs + sets up the secondary renderer explicitly (it is a heavy, rig-only GPU build) and binds it here.
- attach_renderer(renderer: Any) None[source]¶
Bind the secondary :class:
Rendererbuilt against this facade.
- ensure_command_pool() Any[source]¶
Create the secondary device’s command pool on first use, return it.
Construction is GPU-free, so the pool is built here the first time the offload path needs to record on the secondary (the secondary
Rendererbuild + every one-shot layout transition readctx.command_pool). Rig- side; never reached on the single-GPU path (no facade is constructed).
- ensure_offscreen_render_pass() Any[source]¶
Create + own an SRU-compatible offscreen render pass on first use.
The secondary
Renderer’s pipelines are built (insetup()) againstfacade.render_pass, so it MUST exist BEFORE the renderer factory runs, and MUST be format-compatible with the per-SRURenderTargetcreated later (R16G16B16A16_SFLOAT colour + D32 depth, samplable). Pipelines use dynamic viewport/scissor, so the placeholder extent does not bind them to a size and the same pipelines serve any SRU size. Rig-side; never reached on the single-GPU path (no facade is constructed).
- register_texture(image_view: Any, *, filter: str = 'linear', mip_count: int = 1) int[source]¶
Register a bindless texture on the SECONDARY device. Mirrors
Engine.register_texture.
- upload_texture_pixels(pixels: numpy.ndarray, width: int, height: int, *, filter: str = 'linear') int[source]¶
Upload RGBA pixels to the SECONDARY device. Mirrors
Engine.upload_texture_pixels.
- update_texture(slot: int, image_view: Any) None[source]¶
Rewrite a bindless slot on the SECONDARY device. Mirrors
Engine.update_texture.
- unregister_texture(slot: int) None[source]¶
Free a bindless slot for reuse. Mirrors
Engine.unregister_texture.
- push_constants(cmd: Any, pipeline_layout: Any, data: bytes | bytearray) None[source]¶
Record
vkCmdPushConstants. Identical toEngine.push_constants.Push constants are recorded into a command buffer, not bound to a device object, so the implementation is device-independent: the secondary’s cmd buffer + its pipeline layout flow in from the secondary renderer.
- abstractmethod capture_frame() Any[source]¶
A secondary never owns the swapchain: capture is a primary-only op.
- destroy() None[source]¶
Free the secondary-device resources this facade owns (not the device).
The secondary
VkDeviceitself is owned + destroyed by- Class:
~.multi_device.MultiDeviceManager; this only releases the command pool, samplers, descriptor pool, uploaded images, and mesh registry the facade created on that device.
- class simvx.graphics.gpu.secondary_engine.SecondaryResidency(facade: simvx.graphics.gpu.secondary_engine.SecondaryRenderContext)[source]¶
Mirrors to a secondary device the meshes + textures an offloaded SRU needs.
A
VkBuffer/ image / descriptor created on the primary device cannot be used on a secondary, so an offloaded SRU’s geometry (vertex/index buffers via the per-device :class:~..renderer.mesh_registry.MeshRegistry) and any textures it samples (re-uploaded into the secondary’s bindless table) must be made resident on the secondary device before the SRU is recorded there.Two cases:
No-texture / vertex-colour SRU (the minimal first rig case): only the meshes need mirroring; :meth:
ensure_meshesdoes that and nothing else is required.samples_texturesisFalse.Textured SRU: additionally the bindless uploads the SRU samples must be mirrored via :meth:
ensure_textures. This requires the source pixels: the primary’s :class:~..materials.texture.TextureManagerretains them only when constructed withretain_pixels=True(the web path) and NOT on desktop, so full desktop textured-SRU residency is FLAGGED as the remaining step (see- meth:
ensure_textures). The structure + mesh path are complete and tested.
The mirror is keyed by the same source identity the primary uses (mesh
id, texture source key) so a mesh/texture shared across SRUs uploads once per secondary device.Initialization
- ensure_meshes(meshes: list[tuple[int, numpy.ndarray, numpy.ndarray]]) dict[int, Any][source]¶
Upload (id, vertices, indices) tuples to the secondary mesh registry.
Returns a map
primary_mesh_id -> secondary MeshHandle. Meshes already resident (same id) are reused, so a shared mesh uploads once per device.vertices/indicesare the same numpy arrays the primary registered; they are device-independent CPU data, re-uploaded into secondary GPU buffers via the per-device :class:MeshRegistry.
- secondary_mesh(primary_mesh_id: int) Any | None[source]¶
The secondary :class:
MeshHandlefor a primary mesh id, orNone.
- ensure_textures(textures: list[tuple[int, numpy.ndarray, int, int]]) dict[int, int][source]¶
Mirror an SRU’s sampled textures into the secondary bindless table.
texturesis(primary_tex_id, rgba_pixels, width, height)per source the SRU samples. Each is uploaded to the secondary device’s bindless table (once per device, keyed by primary tex id) and the returned map gives the secondary slot to rewrite material texture ids against.RESIDENCY GAP (flagged): the desktop
TextureManagerdoes NOT retain source pixels (retain_pixels=False), so the caller cannot in general recoverrgba_pixelsfor an arbitrary already-uploaded primary texture. The remaining step for full desktop textured-SRU residency is to capture the source pixels at primary-upload time (or read them back from the primary image) and feed them here. Until then, route only no-texture / vertex-colour SRUs to a secondary (samples_texturesFalse); a textured SRU stays on the primary. This method itself is complete: given the pixels it mirrors correctly, and is unit-tested with a fake uploader.