simvx.graphics.engine

Top-level engine entry point.

Module Contents

Classes

CubemapHandle

Opaque handle to a GPU cubemap returned by :meth:Engine.load_cubemap.

DeviceState

Lifecycle of the Vulkan logical device, mirroring the web GpuContext.

Engine

Graphics engine: owns the window, GPU context, and render loop.

Data

API

simvx.graphics.engine.__all__

[‘CubemapHandle’, ‘Engine’]

simvx.graphics.engine.log

‘getLogger(…)’

class simvx.graphics.engine.CubemapHandle[source]

Opaque handle to a GPU cubemap returned by :meth:Engine.load_cubemap.

Pass to :meth:~simvx.graphics.renderer.forward.Renderer.set_skybox to install it as the scene skybox / IBL source. The engine takes ownership of the underlying Vulkan resources and destroys them at shutdown.

view: Any

None

sampler: Any

None

image: Any

None

memory: Any

None

class simvx.graphics.engine.DeviceState[source]

Bases: enum.Enum

Lifecycle of the Vulkan logical device, mirroring the web GpuContext.

BOOTING before the first device is created; READY while rendering; LOST when VK_ERROR_DEVICE_LOST is observed; RECOVERING while a rebuild is in flight; DESTROYED after shutdown. The legacy _device_lost bool remains the per-frame submission gate; this state is the coarser machine the recovery path drives.

BOOTING

‘booting’

READY

‘ready’

LOST

‘lost’

RECOVERING

‘recovering’

DESTROYED

‘destroyed’

__new__(value)
__repr__()
__str__()
__dir__()
__format__(format_spec)
__hash__()
__reduce_ex__(proto)
__deepcopy__(memo)
__copy__()
name()
value()
class simvx.graphics.engine.Engine(width: int = 1280, height: int = 720, title: str = 'SimVX', backend: str | None = None, renderer: str = 'deferred', max_textures: int = MAX_TEXTURES, visible: bool = True, vsync: bool = False, target_fps: int | None = None)[source]

Graphics engine: owns the window, GPU context, and render loop.

Initialization

property ctx: simvx.graphics.gpu.context.GPUContext | None[source]

GPU context holding device, physical_device, queues, and command pool.

None until run() invokes _init_vulkan().

property multi_device: Any[source]

The :class:~simvx.graphics.gpu.multi_device.MultiDeviceManager.

None until run() invokes _init_vulkan(). On the single-GPU / unopted path it is a single-slot passthrough (multi_gpu is False).

property render_pass: simvx.graphics.types._VkRenderPass | None[source]

Main render pass. None until run() invokes _init_vulkan().

property extent: tuple[int, int] | None[source]

Swapchain extent. None until run() invokes _init_vulkan().

property content_scale: tuple[float, float][source]

HiDPI content scale (e.g. (2.0, 2.0) on a 200% display).

property vsync: bool[source]

Whether vertical sync is currently enabled.

set_vsync(value: bool) None[source]

Toggle vsync at runtime by recreating the swapchain with a new present mode.

No-op if the requested state matches the current one. Before the engine has a swapchain (pre-run()) the value is stored and used at boot.

property pre_render_callback: collections.abc.Callable[[simvx.graphics.types._VkCommandBuffer], None] | None[source]

Callback invoked after vkBeginCommandBuffer but before the main render pass.

property shader_dir: pathlib.Path[source]
property current_frame: int[source]

Index of the frame-in-flight slot currently being recorded (0..FRAMES_IN_FLIGHT-1).

Stable across a frame’s recording; advances at present. Per-frame ring buffers index their current slot by this so a write never lands on memory a still-pending previous frame reads.

property current_timestamp_pool: simvx.graphics.gpu.timestamp_pool.TimestampPool | None[source]

Return the TimestampPool for the in-flight frame currently recording.

None if the device does not support timestamp queries (the pool list is left empty by _init_vulkan in that case). Renderers wrap each pass with pool.begin(cmd, label) / pool.end(cmd, label).

property mesh_registry: simvx.graphics.renderer.mesh_registry.MeshRegistry[source]

Get mesh registry (lazy init).

property capabilities: simvx.graphics.gpu.capabilities.RenderCapabilities | None[source]

Immutable host + GPU capability snapshot (set during init_vulkan).

None before Vulkan initialisation. Read-only: configuration flows through WorldEnvironment / App, never through this object.

property texture_manager: simvx.graphics.materials.texture.TextureManager[source]

Get texture manager (lazy init).

property batch: simvx.graphics.renderer.gpu_batch.GPUBatch[source]

Get the GPU batch renderer (lazy init).

property renderer: simvx.graphics.renderer.forward.Renderer[source]

Get the active renderer (creates the default renderer if none exists).

create_renderer(renderer_type: str = 'forward') simvx.graphics.renderer.forward.Renderer[source]

Create and initialize a renderer.

property texture_descriptor_layout: simvx.graphics.types._VkDescriptorSetLayout[source]

Get (lazily init) the texture descriptor set layout.

property texture_descriptor_set: simvx.graphics.types._VkDescriptorSet[source]

Get the texture descriptor set (set 1).

create_render_target(width: int, height: int, use_depth: bool = True) simvx.graphics.renderer.render_target.RenderTarget[source]

Create an offscreen render target for render-to-texture.

load_cubemap(paths_or_hdr: list[str] | str | None = None, *, colour: tuple[float, float, float] | None = None, face_size: int = 256, faces: list | None = None) simvx.graphics.engine.CubemapHandle[source]

Load a cubemap from 6 face images, an equirectangular HDR, or a solid colour.

Args: paths_or_hdr: Either a list of 6 face paths [+X, -X, +Y, -Y, +Z, -Z] for a pre-split cubemap, or a single .hdr (Radiance RGBE) equirectangular file projected onto 6 faces on the CPU. None falls back to colour. colour: RGB triple in 0..1 used when no paths are provided. face_size: Per-face resolution when projecting from an HDR equirect.

Returns: A :class:CubemapHandle; hand this to :meth:Renderer.set_skybox to install it.

register_lut(tex_id: int, lut_data: numpy.ndarray) int[source]

Register a 3D colour-grading LUT under tex_id.

lut_data is an (size, size, size, 4) uint8 array (e.g. from simvx.core.colour_grading.generate_warm_lut). Select it at render time with WorldEnvironment.lut_tex_id = tex_id and lut_enabled = True. The LUT is a 3D rgba8 image sampled post-tonemap, matching the web runtime. Returns tex_id.

register_texture(image_view: simvx.graphics.types._VkImageView, *, filter: str = 'linear', mip_count: int = 1) int[source]

Register a texture (image view) into the bindless array.

Returns the texture index. Reuses slots freed by

Meth:

unregister_texture; otherwise allocates a new slot from the high-water mark.

filter selects the bound sampler: "linear" (default, bilinear) or "nearest" (point sampling, the right choice for pixel-art sprites). The same image view can be registered twice with different filter modes to give the same texture two bindless indices, which is how :class:SceneAdapter honours per-Sprite2D filter.

mip_count is the number of mip levels in the view. The default 1 binds the shared maxLod=0 sampler (single-mip behaviour unchanged); a value > 1 binds a cached maxLod=mip_count-1 sampler so the whole mip chain is sampled (without it every level beyond 0 is unreachable).

update_texture(slot: int, image_view: simvx.graphics.types._VkImageView) None[source]

Rewrite the descriptor at an existing bindless slot.

Used when the backing image view changes (e.g. render-target resize) but the slot id must remain stable so callers that captured it don’t need re-notification.

unregister_texture(slot: int) None[source]

Release a bindless texture slot for reuse by a later register_texture.

The descriptor at the slot is left pointing at its old view until something new is bound there; the slot is simply marked free.

upload_texture_pixels(pixels: numpy.ndarray, width: int, height: int, *, filter: str = 'linear', colour_space: str = 'srgb', mipmaps: bool = False) int[source]

Upload raw RGBA pixel data to GPU. Returns the bindless texture index.

filter is forwarded to :meth:register_texture; pass "nearest" for pixel-art sprites that should keep crisp edges when scaled.

colour_space selects the sampled image view format (linear-workflow contract, Stage 1): "srgb" (default) makes a VK_FORMAT_R8G8B8A8_SRGB view so the GPU decodes the stored sRGB bytes to linear on sample, round-tripping correctly through the sRGB swapchain (the right choice for colour textures: 2D sprites, tileset atlases, Draw2D images). "linear" keeps the VK_FORMAT_R8G8B8A8_UNORM view: the choice for data textures that are not perceptual colour (normal/data maps) and for the 3D material albedo path, which currently treats its uploaded bytes as already-linear. The pixel bytes uploaded to memory are identical in both cases; only the view’s interpretation differs.

mipmaps=True allocates the full mip chain and generates every level on the GPU with a linear-filtered vkCmdBlitImage chain, then binds the maxLod=chain-1 sampler so minification actually reaches the chain. Falls back to a single mip when the view format lacks linear-blit support (so exotic formats degrade gracefully) or when the image is already 1x1. The default False keeps the historical single-mip upload byte-identical.

format_supported(vk_format: int) bool[source]

Return True if vk_format can be sampled from an optimal-tiled image.

The authoritative per-format gate consulted by the compressed-texture upload path: the coarse textureCompressionBC device feature must be present AND the specific BC format must report SAMPLED_IMAGE in its optimal-tiling features.

supports_compressed_format(vk_format: int) bool[source]

Whether a block-compressed vk_format is usable on this device.

Gates on the coarse family feature for vk_format’s block family (BC / ASTC-LDR / ETC2, the single source of truth in self._capabilities) AND the per-format sampled-image feature. Consulted by TextureManager before dispatching a compressed upload.

compressed_caps() dict[str, bool][source]

Coarse block-compression family support for this device.

Returns the texture_compression_{bc,etc2,astc_ldr} subset of the probed capabilities. Consulted by TextureManager to pick a UASTC transcode target (BC7 -> ASTC-4x4 -> ETC2). Each coarse bit is necessary but not sufficient: a per-format SAMPLED check (format_supported) is the second gate. Public accessor so callers never reach into the private _capabilities.

upload_texture_blocks(blocks: list[bytes], width: int, height: int, fmt: int, block_size: int, *, filter: str = 'linear') int[source]

Upload block-compressed mip data to GPU. Returns the bindless texture index.

Parallel to :meth:upload_texture_pixels for the uncompressed RGBA8 path: builds the image view with the COMPRESSED fmt and a levelCount spanning all supplied mips, then registers it into the bindless array and tracks it for teardown exactly like the uncompressed path.

load_mesh(file_path: str) simvx.graphics.types.MeshHandle[source]

Load a glTF file’s first mesh primitive from disk and register it.

Returns: MeshHandle for use in rendering.

create_vertex_buffer(vertices: numpy.ndarray) tuple[Any, simvx.graphics.types._VkDeviceMemory][source]

Create a GPU vertex buffer and upload data. Returns (buffer, memory).

create_index_buffer(indices: numpy.ndarray) tuple[Any, simvx.graphics.types._VkDeviceMemory][source]

Create a GPU index buffer and upload data. Returns (buffer, memory).

create_ssbo(data: numpy.ndarray) tuple[Any, simvx.graphics.types._VkDeviceMemory][source]

Create an SSBO and upload data. Returns (buffer, memory).

update_ssbo(memory: simvx.graphics.types._VkDeviceMemory, data: numpy.ndarray) None[source]

Update SSBO contents in-place via mapped memory.

create_descriptor_pool(max_sets: int = 4) simvx.graphics.types._VkDescriptorPool[source]

Create a descriptor pool.

create_descriptor_set_layout(binding_count: int = 3) simvx.graphics.types._VkDescriptorSetLayout[source]

Create a descriptor set layout with N SSBO bindings.

allocate_descriptor_set(pool: simvx.graphics.types._VkDescriptorPool, layout: simvx.graphics.types._VkDescriptorSetLayout) simvx.graphics.types._VkDescriptorSet[source]

Allocate a descriptor set from pool.

write_descriptor_ssbo(descriptor_set: simvx.graphics.types._VkDescriptorSet, binding: int, buffer: Any, size: int) None[source]

Bind an SSBO buffer to a descriptor set binding.

compile_and_load_shader(name: str) simvx.graphics.types._VkShaderModule[source]

Compile a shader from SHADER_DIR and return its module.

update_vertex_buffer(memory: simvx.graphics.types._VkDeviceMemory, data: numpy.ndarray) None[source]

Update vertex buffer contents in-place via mapped memory.

push_constants(cmd: simvx.graphics.types._VkCommandBuffer, pipeline_layout: simvx.graphics.types._VkPipelineLayout, data: bytes | bytearray) None[source]

Push constant data (view + proj).

enable_picking(descriptor_layout: simvx.graphics.types._VkDescriptorSetLayout, descriptor_set: simvx.graphics.types._VkDescriptorSet) None[source]

Initialize the GPU pick pass for mouse picking.

pick_entity(x: int, y: int, view_proj_data: bytes, vertex_buffer: Any, index_buffer: Any, index_count: int, instance_count: int) int[source]

Read entity ID at screen position (x, y). Returns entity index or -1.

set_selected_objects(selected: list[tuple[simvx.graphics.types.MeshHandle, numpy.ndarray, int]]) None[source]

Set the list of selected objects to highlight with outlines.

Args: selected: List of (mesh_handle, transform_4x4, material_id) tuples.

clear_selected_objects() None[source]

Clear all selection outlines.

property outline_pass: simvx.graphics.renderer.outline_pass.OutlinePass | None[source]

Access outline pass for configuration (colour, width, enabled).

create_text_texture(font: str | None = None, size: int = 32, width: int = 256, height: int = 64) Any[source]

Create a texture with rendered text for use on 3D objects.

Returns a TextTexture with .text, .colour, and .texture_index properties. Setting .text or .colour re-renders and re-uploads the texture.

Web parity (simvx.web EngineStub.create_text_texture): the font argument is a no-op on web (text is rasterized from the atlas baked at export time), and texture_index is reassigned on every re-render there (the bindless slot is reused in place on desktop). So set .text/.colour before binding texture_index to a material for identical results on both backends – which is how the examples author it.

set_key_callback(callback: collections.abc.Callable[[int, int, int], None]) None[source]

Register callback(key, action, mods) for keyboard events.

set_mouse_button_callback(callback: collections.abc.Callable[[int, int, int], None]) None[source]

Register callback(button, action, mods) for mouse button events.

set_cursor_pos_callback(callback: collections.abc.Callable[[float, float], None]) None[source]

Register callback(x, y) for cursor position events.

set_scroll_callback(callback: collections.abc.Callable[[float, float], None]) None[source]

Register callback(x_offset, y_offset) for scroll wheel events.

set_char_callback(callback: collections.abc.Callable[[int], None]) None[source]

Register callback(codepoint) for character input events.

set_cursor_shape(shape: int) None[source]

Set cursor shape. 0=arrow, 1=ibeam, 2=crosshair, 3=hand, 4=hresize, 5=vresize.

set_mouse_capture(mode: int) None[source]

Apply a MouseCaptureMode int to the OS cursor via the active window backend.

property cursor_pos: tuple[float, float][source]

Current cursor position in screen coordinates.

run(callback: collections.abc.Callable[[], None] | None = None, setup: collections.abc.Callable[[], None] | None = None, render: collections.abc.Callable[[simvx.graphics.types._VkCommandBuffer, tuple[int, int]], None] | None = None, pre_render: collections.abc.Callable[[simvx.graphics.types._VkCommandBuffer], None] | None = None, cleanup: collections.abc.Callable[[], None] | None = None, frame_driver: collections.abc.Callable[[], None] | None = None, pre_shutdown: collections.abc.Callable[[], None] | None = None) None[source]

Start the main loop.

Args: callback: Legacy per-frame callback (called before draw). setup: Called once after Vulkan init, before the loop. render: Custom render callback receiving (command_buffer, extent). If provided, replaces the built-in triangle rendering. pre_render: Called with command_buffer after vkBeginCommandBuffer but before the main render pass. Use for offscreen passes. cleanup: Called once when the loop exits, after the final vkDeviceWaitIdle but before shutdown() destroys the device. The right place to release caller-owned GPU resources (offscreen render targets, bindless slots) that must outlive the loop but die before the device. pre_shutdown: Called once when the loop exits, BEFORE the final vkDeviceWaitIdle. The pipelined driver stops + joins its render thread here so no GPU work is issued against the device while/after it is being torn down. None (default) is a no-op, unchanged. frame_driver: Pipelined-mode hook replacing the per-frame self._draw_frame() call on the MAIN thread. When given, the engine loop runs poll_events -> callback() ->              frame_driver() and never touches the GPU itself: the driver hands the frame’s render packet to a render thread that owns all GPU work. None (default) keeps the synchronous _draw_frame path, byte-identical to before.

begin(*, setup: collections.abc.Callable[[], None] | None = None, render: collections.abc.Callable[[simvx.graphics.types._VkCommandBuffer, tuple[int, int]], None] | None = None, pre_render: collections.abc.Callable[[simvx.graphics.types._VkCommandBuffer], None] | None = None) None[source]

Create the window + Vulkan device and run setup.

The externally-driven counterpart to :meth:run: pair with :meth:step and :meth:end to advance frames under a caller-owned clock (the agent live-session / editor viewport). run is begin + a step loop + end.

step(*, callback: collections.abc.Callable[[], None] | None = None, frame_driver: collections.abc.Callable[[], None] | None = None) bool[source]

Run one frame iteration (poll -> callback -> draw).

Returns False when the loop should stop (quit requested or window closed), True otherwise (including a paused/backgrounded frame, which is skipped).

end(*, pre_shutdown: collections.abc.Callable[[], None] | None = None, cleanup: collections.abc.Callable[[], None] | None = None) None[source]

Tear down: quiesce the render thread, wait-idle, run cleanup, shutdown.

arm_capture() None[source]

Arm a one-shot readback of the NEXT _draw_frame before it presents.

The captured pixels are retrieved with :meth:take_captured_frame. This is the acquire-before-transition capture path: the readback happens while the swapchain image is still acquired, so it never trips the “presentable image must be acquired” validation rule that the post-present :meth:capture_frame does. Used by the synchronous headless capture sink.

take_captured_frame() numpy.ndarray | None[source]

Return (and clear) the frame stashed by an armed capture, or None.

capture_frame() numpy.ndarray[source]

Capture the last rendered framebuffer as an RGBA numpy array.

Returns (height, width, 4) uint8 array. Must be called after _draw_frame().

property window_size: tuple[int, int][source]

Current window (width, height). Writable to programmatically resize.

shutdown() None[source]

Clean up all resources.