simvx.graphics.gpu.commands¶
Command pool and command buffer helpers.
Module Contents¶
Classes¶
A re-entrant lock that can answer whether the CALLING thread holds it. |
|
Manages a command pool and provides command buffer allocation. |
|
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.RLockcannot. It documents onlyacquire,releaseandlocked(), andlocked()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_ownera caller might reach for does not exist on CPython 3.13+: the C implementation exposes no such attribute, sogetattr(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.
heldis then a thread-local read (about 85 ns) and is true exactly when this thread is inside awith. Nesting is counted rather than flagged, so an innerwithreleasing does not report the outer one as gone.Drop-in for
threading.RLock:with lock:,acquireandreleaseall 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
withon this lock.
- acquire(blocking: bool = True, timeout: float = -1) bool¶
- release() None¶
- __enter__() simvx.graphics.gpu.commands.TrackedRLock¶
- class simvx.graphics.gpu.commands.CommandContext(device: Any, queue_family_index: int)¶
Manages a command pool and provides command buffer allocation.
A
VkCommandPoolcarries its own external-synchronisation rule, separate from the one on the queue:vkAllocateCommandBuffersandvkFreeCommandBuffersexternally synchronise the pool, andvkBeginCommandBuffer, everyvkCmd*andvkEndCommandBufferexternally 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:
guardis how that rule is checked rather than merely written down. Install a callable returning a refusal message (orNoneto allow) and every read of :attr:poolconsults 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 readsNoneand pays a singleis 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.textsetter 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:
CommandContextfor the rule. The frame pool going back to single-thread use is what restores the invariantrenderer/render_threadalready 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:
poolraises 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
withon this context.Raises: RuntimeError: if the pool has not been created, or if the calling thread is not inside the
withblock 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.