simvx.graphics.engine¶
Top-level engine entry point.
Module Contents¶
Classes¶
Opaque handle to a GPU cubemap returned by :meth: |
|
Lifecycle of the Vulkan logical device, mirroring the web GpuContext. |
|
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¶
Opaque handle to a GPU cubemap returned by :meth:
Engine.load_cubemap.Pass to :meth:
~simvx.graphics.renderer.forward.Renderer.set_skyboxto install it as the scene skybox / IBL source. The engine takes ownership of the underlying Vulkan resources and destroys them at shutdown, whether or not the handle is ever installed: the engine records every handle it hands out,set_skyboxdiscards the one it claims, and shutdown frees the rest.Call :meth:
releaseto free one earlier than that. A level that loads a sky it turns out not to need is the case: holding the handle to shutdown holds an image, a view, a sampler and their memory for the life of the process.- view: Any¶
None
- sampler: Any¶
None
- image: Any¶
None
- memory: Any¶
None
- engine: Any¶
‘field(…)’
- extent: tuple[int, int] | None¶
‘field(…)’
- release() None¶
Free this cubemap’s GPU resources now, rather than at shutdown.
Safe to call more than once, and safe on a handle the renderer has already claimed: both are no-ops, because the engine drops a handle from its record the moment something else becomes responsible for it. The free is deferred through the retire queue, so a frame still sampling the cubemap completes first.
- class simvx.graphics.engine.DeviceState¶
Bases:
enum.EnumLifecycle of the Vulkan logical device, mirroring the web GpuContext.
BOOTINGbefore the first device is created;READYwhile rendering;LOSTwhenVK_ERROR_DEVICE_LOSTis observed;RECOVERINGwhile a rebuild is in flight;DESTROYEDafter shutdown. The legacy_device_lostbool 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)¶
Graphics engine: owns the window, GPU context, and render loop.
Initialization
- clear_colour: list[float]¶
[0.0, 0.0, 0.0, 1.0]
The window background in authored sRGB space, with linear alpha.
Every attachment that clears to this decodes it for its own format, so the value held here is the one the author wrote and not the one the GPU receives. The fourth component is alpha and passes through untouched: the scene adapter and the secondary-device path both read it as the “draw the sky” switch.
- property ctx: simvx.graphics.gpu.context.GPUContext | None¶
GPU context holding device, physical_device, queues, and command pool.
Noneuntilrun()invokes_init_vulkan().
- property retire: simvx.graphics.gpu.retire.RetireQueue¶
The queue that holds a GPU free until the frames naming it have retired.
Hand a destructor here rather than calling it from anywhere a submitted command buffer might still reference the resource. See
- Class:
~.gpu.retire.RetireQueuefor what the queue does and does not prove.
- property multi_device: Any¶
The :class:
~simvx.graphics.gpu.multi_device.MultiDeviceManager.Noneuntilrun()invokes_init_vulkan(). On the single-GPU / unopted path it is a single-slot passthrough (multi_gpuisFalse).
- property render_pass: simvx.graphics.types._VkRenderPass | None¶
Main render pass.
Noneuntilrun()invokes_init_vulkan().
- property extent: tuple[int, int] | None¶
Swapchain extent.
Noneuntilrun()invokes_init_vulkan().
- property content_scale: tuple[float, float]¶
HiDPI content scale (e.g.
(2.0, 2.0)on a 200% display).
- property vsync: bool¶
Whether vertical sync is currently enabled.
- set_vsync(value: bool) None¶
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 target_fps: int | None¶
Frame cap in Hz, or
Nonefor uncapped.Writable while the loop is running: :meth:
runrecomputes its frame budget from this field every iteration, so a game’s options menu can apply a new cap without a relaunch. Two limits worth knowing: under vsync a cap above the refresh rate does nothing (the budget sleep sits on top of a blocking present), and the externally driven- Meth:
begin/ :meth:step/ :meth:endpath is caller-clocked and ignores the cap by design.
- property pre_render_callback: collections.abc.Callable[[simvx.graphics.types._VkCommandBuffer], None] | None¶
Callback invoked after
vkBeginCommandBufferbut before the main render pass.
- property shader_dir: pathlib.Path¶
- property current_frame: int¶
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¶
Return the TimestampPool for the in-flight frame currently recording.
Noneif the device does not support timestamp queries (the pool list is left empty by_init_vulkanin that case). Renderers wrap each pass withpool.begin(cmd, label)/pool.end(cmd, label).
- property mesh_registry: simvx.graphics.renderer.mesh_registry.MeshRegistry¶
Get mesh registry (lazy init).
- property capabilities: simvx.graphics.gpu.capabilities.RenderCapabilities | None¶
Immutable host + GPU capability snapshot (set during
init_vulkan).Nonebefore Vulkan initialisation. Read-only: configuration flows throughWorldEnvironment/App, never through this object.
- property texture_manager: simvx.graphics.materials.texture.TextureManager¶
Get texture manager (lazy init).
- property batch: simvx.graphics.renderer.gpu_batch.GPUBatch¶
Get the GPU batch renderer (lazy init).
- property renderer: simvx.graphics.renderer.forward.Renderer¶
Get the active renderer (creates the default renderer if none exists).
- create_renderer(renderer_type: str = 'forward') simvx.graphics.renderer.forward.Renderer¶
Create and initialize a renderer.
- property texture_descriptor_layout: simvx.graphics.types._VkDescriptorSetLayout¶
Get (lazily init) the texture descriptor set layout.
- property texture_descriptor_set: simvx.graphics.types._VkDescriptorSet¶
Get the texture descriptor set (set 1).
- create_render_target(width: int, height: int, use_depth: bool = True) simvx.graphics.renderer.render_target.RenderTarget¶
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¶
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.Nonefalls back tocolour. 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_skyboxto install it, or :meth:CubemapHandle.releaseto free it. Either way the engine frees it at device teardown if nothing else has.
- update_cubemap_faces(handle: simvx.graphics.engine.CubemapHandle, faces: list[numpy.ndarray]) None¶
Rewrite the six faces of an existing cubemap in place.
The handle keeps its identity, so the skybox pipeline, the descriptor that names the view and the IBL pass’s own binding all stay valid: the only thing that changes is the texels. That is the whole point. An animated sky changes two colours per frame and nothing about the cube’s dimensions or format, and rebuilding image, memory, view, sampler,
SkyboxPassand its pipeline around that is work with no consequence.facesmust be six(h, w, 4)float32 arrays of the same shape as the ones the cube was loaded with. A different shape is refused rather than silently reallocated: the caller holding this handle is the one that would have to hear about a new view, and it cannot.Waits out the frames in flight first. The cube is a sampled image the submitted-and-unfinished frames may still be reading, so writing it from the host with no wait is a cross-frame write-after-read – the sky would tear or flicker under load and nothing would say why. The wait is one prior frame’s fence rather than a device drain, which is what the rebuild path did.
Raises: ValueError: wrong number of faces, or a face whose shape does not match the cube’s.
- register_lut(tex_id: int, lut_data: numpy.ndarray) int¶
Register a 3D colour-grading LUT under
tex_id.lut_datais an(size, size, size, 4)uint8 array (e.g. fromsimvx.core.colour_grading.generate_warm_lut). Select it at render time withWorldEnvironment.lut_tex_id = tex_idandlut_enabled = True. The LUT is a 3Drgba8image sampled post-tonemap, matching the web runtime. Returnstex_id.Callable from tick-time game code, so the staging copy is recorded from the upload pool with its lock held, exactly as :meth:
load_cubemapdoes. The frame pool belongs to the pipelined render thread while it is recording, and a pool cannot be allocated from and recorded into by two threads at once.
- register_texture(image_view: simvx.graphics.types._VkImageView, *, filter: str = 'linear', mip_count: int = 1) int¶
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.
filterselects 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:SceneAdapterhonours per-Sprite2Dfilter.mip_countis 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).Raises: ValueError: the array is full, i.e. the high-water mark has reached
max_texturesand no slot has been freed.
- update_texture(slot: int, image_view: simvx.graphics.types._VkImageView) None¶
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.
- texture_binding(slot: int) tuple[simvx.graphics.types._VkImageView, simvx.graphics.types._VkSampler] | None¶
The (image view, sampler) pair a bindless slot resolves to, or None.
Every other pass reads textures through the bindless array by index. The custom-shader material group cannot: its descriptor layout has to match the WebGPU bind-group layout the same shader gets in the browser, where a texture and a sampler are separate bindings. This is the one accessor that hands those two handles back for a slot the texture manager minted.
- material_sampler() simvx.graphics.types._VkSampler¶
The sampler every declared sampler binding of a custom material gets.
One linear, repeating, mip-sampling sampler, and deliberately the same one the browser runtime binds. A custom shader declares its samplers separately from its textures, so a sampler cannot inherit the filter of any one texture it reads; what it can do is be identical on both backends. LOD is left unclamped (Vulkan bounds it by the view’s own level count, as WebGPU’s default
lodMaxClampdoes), so a mip-chained texture minifies through its chain rather than shimmering off level 0.
- unregister_texture(slot: int) None¶
Release a bindless texture slot, repointing it at the 1x1 dummy texture.
The descriptor is rewritten before the slot goes on the free list, so a material that still holds the index samples a live view rather than a destroyed one.
descriptorBindingPartiallyBoundexcuses a descriptor the shader never reaches; it does not excuse one it still indexes.Call this before destroying the view. The repoint is legal under the array’s
UPDATE_AFTER_BINDflag precisely because both the old and the new value are live at that instant; unregistering a view already destroyed writes nothing dangerous but leaves the window this method exists to close.
- upload_texture_pixels(pixels: numpy.ndarray, width: int, height: int, *, filter: str = 'linear', colour_space: str = 'srgb', mipmaps: bool = False) int¶
Upload raw RGBA pixel data to GPU. Returns the bindless texture index.
filteris forwarded to :meth:register_texture; pass"nearest"for pixel-art sprites that should keep crisp edges when scaled.colour_spaceselects the sampled image view format, keeping shading in linear space:"srgb"(default) makes aVK_FORMAT_R8G8B8A8_SRGBview 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 theVK_FORMAT_R8G8B8A8_UNORMview: 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=Trueallocates the full mip chain and generates every level on the GPU with a linear-filteredvkCmdBlitImagechain, 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.
- update_texture_pixels(slot: int, pixels: numpy.ndarray, width: int, height: int, *, filter: str | None = None, colour_space: str | None = None, mipmaps: bool | None = None) int¶
Replace the pixels behind an existing bindless slot. Returns the slot.
The slot id stays valid, so everything already drawing with it – a sprite’s published handle, a material’s texture index – shows the new image without being told. This is what makes
simvx.core.graphics.Texture.update()reach the GPU, and it is the call an IDE-style widget re-rasterising into a cached texture wants.Omitted keywords keep what the slot was registered with. That matters: an update that re-derived the sampler would quietly turn a
filter="nearest"pixel-art texture bilinear, and drop a mip chain’s maxLod back to 0.Same dimensions and format (the common case: a widget redrawing itself) writes straight into the existing image, so nothing is allocated and nothing is freed. A changed size or colour space rebuilds the image behind the slot and releases the old one, rather than leaking it.
That rebuild belongs to the widgets that own both ends of a slot – the IDE minimap re-rasterises at the panel’s width and re-uploads at the new size, keeping its own quad in step. It is not how a node’s texture resizes:
Texture.update()refuses a size change, because the nodes drawing a texture measured it once and would not hear about one. The exception is a texture whose size nothing has ever been able to measure, which has no size to be held to; a node showing one is in the same position and reads its dimensions from the upload.
- format_supported(vk_format: int) bool¶
Return True if
vk_formatcan be sampled from an optimal-tiled image.The authoritative per-format gate consulted by the compressed-texture upload path: the coarse
textureCompressionBCdevice feature must be present AND the specific BC format must reportSAMPLED_IMAGEin its optimal-tiling features.
- supports_compressed_format(vk_format: int) bool¶
Whether a block-compressed
vk_formatis 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 inself._capabilities) AND the per-format sampled-image feature. Consulted byTextureManagerbefore dispatching a compressed upload.
- compressed_caps() dict[str, bool]¶
Coarse block-compression family support for this device.
Returns the
texture_compression_{bc,etc2,astc_ldr}subset of the probed capabilities. Consulted byTextureManagerto 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¶
Upload block-compressed mip data to GPU. Returns the bindless texture index.
Parallel to :meth:
upload_texture_pixelsfor the uncompressed RGBA8 path: builds the image view with the COMPRESSEDfmtand 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¶
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]¶
Create a GPU vertex buffer and upload data. Returns (buffer, memory).
- create_index_buffer(indices: numpy.ndarray) tuple[Any, simvx.graphics.types._VkDeviceMemory]¶
Create a GPU index buffer and upload data. Returns (buffer, memory).
- create_ssbo(data: numpy.ndarray) tuple[Any, simvx.graphics.types._VkDeviceMemory]¶
Create an SSBO and upload data. Returns (buffer, memory).
- update_ssbo(memory: simvx.graphics.types._VkDeviceMemory, data: numpy.ndarray) None¶
Update SSBO contents in-place via mapped memory.
- create_descriptor_pool(max_sets: int = 4) simvx.graphics.types._VkDescriptorPool¶
Create a descriptor pool.
- create_descriptor_set_layout(binding_count: int = 3) simvx.graphics.types._VkDescriptorSetLayout¶
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¶
Allocate a descriptor set from pool.
- write_descriptor_ssbo(descriptor_set: simvx.graphics.types._VkDescriptorSet, binding: int, buffer: Any, size: int) None¶
Bind an SSBO buffer to a descriptor set binding.
- compile_and_load_shader(name: str) simvx.graphics.types._VkShaderModule¶
Compile a shader from SHADER_DIR and return its module.
- update_vertex_buffer(memory: simvx.graphics.types._VkDeviceMemory, data: numpy.ndarray) None¶
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¶
Push constant data (view + proj).
- enable_picking(descriptor_layout: simvx.graphics.types._VkDescriptorSetLayout, descriptor_set: simvx.graphics.types._VkDescriptorSet) None¶
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¶
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¶
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¶
Clear all selection outlines.
- property outline_pass: simvx.graphics.renderer.outline_pass.OutlinePass | None¶
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¶
Create a texture with rendered text for use on 3D objects.
Returns a TextTexture with
.text,.colourand.texture_index. Setting.textor.colourre-rasterises and writes the new pixels into the image the slot already holds, sotexture_indexis stable from the first render onward: bind it to a material once and later changes are seen through the same slot. Call.destroy()to release the slot and the image; without it they live until device teardown.Web parity (
simvx.webEngineStub.create_text_texture): thefontargument is a no-op on web (text is rasterised from the atlas baked at export time). Everything else matches,texture_indexstability anddestroy()included.
- set_key_callback(callback: collections.abc.Callable[[int, int, int], None]) None¶
Register callback(key, action, mods) for keyboard events.
- set_mouse_button_callback(callback: collections.abc.Callable[[int, int, int], None]) None¶
Register callback(button, action, mods) for mouse button events.
- set_cursor_pos_callback(callback: collections.abc.Callable[[float, float], None]) None¶
Register callback(x, y) for cursor position events.
- set_scroll_callback(callback: collections.abc.Callable[[float, float], None]) None¶
Register callback(x_offset, y_offset) for scroll wheel events.
- set_char_callback(callback: collections.abc.Callable[[int], None]) None¶
Register callback(codepoint) for character input events.
- set_cursor_shape(shape: int) None¶
Set cursor shape. 0=arrow, 1=ibeam, 2=crosshair, 3=hand, 4=hresize, 5=vresize.
- set_mouse_capture(mode: int) None¶
Apply a MouseCaptureMode int to the OS cursor via the active window backend.
- property cursor_pos: tuple[float, float]¶
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¶
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
vkDeviceWaitIdlebut beforeshutdown()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 finalvkDeviceWaitIdle. 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-frameself._draw_frame()call on the MAIN thread. When given, the engine loop runspoll_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_framepath, 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¶
Create the window + Vulkan device and run
setup.The externally-driven counterpart to :meth:
run: pair with :meth:stepand :meth:endto advance frames under a caller-owned clock (the agent live-session / editor viewport).runisbegin+ a step loop +end.
- step(*, callback: collections.abc.Callable[[], None] | None = None, frame_driver: collections.abc.Callable[[], None] | None = None) bool¶
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¶
Tear down: quiesce the render thread, wait-idle, run cleanup, shutdown.
- arm_capture() None¶
Arm a one-shot readback of the NEXT
_draw_framebefore 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_framedoes. Used by both capture paths: the synchronous loop before it hands the frame to the driver, and the pipelined render thread before it records a packet’s frame.One shot at exactly one frame, and the next
_draw_framespends it whether or not that frame reaches its present, so a frame abandoned to a device loss or a swapchain rebuild does not push the arm onto the frame after it.
- take_captured_frame() numpy.ndarray | None¶
Return (and clear) the frame stashed by an armed capture, or
None.
- capture_frame() numpy.ndarray¶
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]¶
Current window
(width, height). Writable to programmatically resize.
- shutdown() None¶
Clean up all resources.