simvx.graphics.gpu.commands

Command pool and command buffer helpers.

Module Contents

Classes

TrackedRLock

A re-entrant lock that can answer whether the CALLING thread holds it.

CommandContext

Manages a command pool and provides command buffer allocation.

UploadContext

A locked command pool for one-shot work issued outside the frame record.

Data

API

simvx.graphics.gpu.commands.__all__

[‘CommandContext’, ‘TrackedRLock’, ‘UploadContext’]

simvx.graphics.gpu.commands.log

‘getLogger(…)’

class simvx.graphics.gpu.commands.TrackedRLock

A re-entrant lock that can answer whether the CALLING thread holds it.

threading.RLock cannot. It documents only acquire, release and locked(), and locked() answers “held by somebody”, which is the wrong question for a mutual-exclusion rule whose violation is one thread reaching a resource another thread is inside. The private _owner a caller might reach for does not exist on CPython 3.13+: the C implementation exposes no such attribute, so getattr(lock, "_owner", None) reports “not held” for every caller and a guard built on it fires on nothing.

So count the depth in a thread-local, in the one place that takes the lock. held is then a thread-local read (about 85 ns) and is true exactly when this thread is inside a with. Nesting is counted rather than flagged, so an inner with releasing does not report the outer one as gone.

Drop-in for threading.RLock: with lock:, acquire and release all behave as before, so a call site that only wants mutual exclusion needs no change and gets the marker for free.

Initialization

__slots__

(‘_lock’, ‘_local’)

property held: bool

True when the calling thread is inside a with on this lock.

acquire(blocking: bool = True, timeout: float = -1) bool
release() None
__enter__() simvx.graphics.gpu.commands.TrackedRLock
__exit__(exc_type: type[BaseException] | None, exc: BaseException | None, tb: types.TracebackType | None) None
class simvx.graphics.gpu.commands.CommandContext(device: Any, queue_family_index: int)

Manages a command pool and provides command buffer allocation.

A VkCommandPool carries its own external-synchronisation rule, separate from the one on the queue: vkAllocateCommandBuffers and vkFreeCommandBuffers externally synchronise the pool, and vkBeginCommandBuffer, every vkCmd* and vkEndCommandBuffer externally synchronise the buffer and the pool it came from. Allocating from a pool that another thread is recording into is undefined behaviour even when every submit is perfectly locked. This class does no locking, so one instance belongs to one thread: the engine’s is the frame-record thread’s. Anything reached from elsewhere wants :class:UploadContext.

Attr:

guard is how that rule is checked rather than merely written down. Install a callable returning a refusal message (or None to allow) and every read of :attr:pool consults it. The engine installs one that refuses a read from a thread which is neither the render thread nor holding the renderer’s frame-state lock, and only while dev checks are on: the guard is a development instrument, and a shipped game reads None and pays a single is not None.

Initialization

property pool: Any

The command pool, subject to :attr:guard.

Raises: RuntimeError: the installed guard refused this caller.

create_pool() None
allocate(count: int = 1) list[Any]
destroy() None
class simvx.graphics.gpu.commands.UploadContext(device: Any, queue_family_index: int)

A locked command pool for one-shot work issued outside the frame record.

Texture uploads, cubemap loads, render-target layout transitions, the pick pass’s readback and swapchain capture all allocate a command buffer, record into it, submit, drain, and free it. Those calls are reached from tick-time application code – a TextTexture.text setter in _process, an editor panel repainting a preview – and in pipelined mode that is the main thread while the render thread is recording the frame. Sharing the frame pool with them is undefined behaviour on the pool, independently of the queue: see

Class:

CommandContext for the rule. The frame pool going back to single-thread use is what restores the invariant renderer/render_thread already claims in prose.

So this is a second pool with a lock covering the whole bounded allocate-record-submit-wait-free span, entered as a context manager::

with engine.upload_ctx as pool:
    cmd = begin_single_time_commands(device, pool)
    ...
    end_single_time_commands(device, queue, pool, cmd)
Attr:

pool raises outside that block rather than handing back a pool the caller has not taken the lock on. That is the same principle as

Class:

~simvx.graphics.gpu.queue.SubmitQueue: a rule the caller has to remember is a rule that gets forgotten, and here forgetting it is a data race that only shows up as corruption under a validation layer. The failure is instead a named error on the first upload, in every configuration.

The lock is re-entrant, so an upload helper that nests inside another (a mip-chain generation inside an image upload) does not deadlock.

Initialization

property device: Any
create_pool() None

Create the underlying pool. Call once, at device creation.

property pool: Any

The command pool, valid only inside a with on this context.

Raises: RuntimeError: if the pool has not been created, or if the calling thread is not inside the with block that would make its use exclusive.

__enter__() Any
__exit__(exc_type: type[BaseException] | None, exc: BaseException | None, tb: types.TracebackType | None) None
destroy() None

Destroy the pool, holding the lock so no upload is mid-span.