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

SecondaryRenderContext

Engine-like facade backed by a secondary :class:DeviceSlot.

SecondaryResidency

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 a CommandContext + a :class:GPUContext on the secondary slot’s device. The expensive GPU build (pipelines, descriptor pools, SSBOs) happens later in Renderer(facade).setup(), on the rig.

Initialization

property ctx: simvx.graphics.gpu.context.GPUContext[source]
property capabilities: simvx.graphics.gpu.capabilities.RenderCapabilities | None[source]
property content_scale: tuple[float, float][source]
property extent: tuple[int, int] | None[source]
property shader_dir: Any[source]
property render_pass: Any[source]
property current_timestamp_pool: Any[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_pixels on the secondary device), so the manager caches

  • resolves against secondary-device bindless slots.

property renderer: Any[source]

The secondary :class:Renderer once bound via :meth:attach_renderer.

Unlike Engine.renderer this 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:Renderer built 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 Renderer build + every one-shot layout transition read ctx.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 (in setup()) against facade.render_pass, so it MUST exist BEFORE the renderer factory runs, and MUST be format-compatible with the per-SRU RenderTarget created 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).

property texture_descriptor_layout: Any[source]
property texture_descriptor_set: Any[source]
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 to Engine.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 VkDevice itself 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_meshes does that and nothing else is required. samples_textures is False.

  • 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.TextureManager retains them only when constructed with retain_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 / indices are 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:MeshHandle for a primary mesh id, or None.

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.

textures is (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 TextureManager does NOT retain source pixels (retain_pixels=False), so the caller cannot in general recover rgba_pixels for 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_textures False); 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.

secondary_texture(primary_tex_id: int) int | None[source]

The secondary bindless slot for a primary tex id, or None.