simvx.graphics.gpu.retire

Deferred GPU-resource frees, clocked on completed frames.

One hazard, stated narrowly: a resource unbound mid-frame may still be read by a command buffer that has been submitted and has not completed, so destroying it is a use-after-free. The answer is to delay the destroy until every frame that could name the resource has retired.

That is all this module does. Repointing a descriptor at a different resource is a different hazard with a different answer (an UPDATE_AFTER_BIND binding, a ring slot no pending frame uses, or a wait), and deferring a free solves none of it. Keeping the two apart is deliberate: see design/render_parallelism_and_multi_gpu.md and packages/graphics/tests/test_update_after_bind_contract.py.

Module Contents

Classes

RetireQueue

Defers a GPU-resource free until every frame that could reference it has retired.

Data

API

simvx.graphics.gpu.retire.log

‘getLogger(…)’

simvx.graphics.gpu.retire.__all__

[‘RetireQueue’]

class simvx.graphics.gpu.retire.RetireQueue(depth: int = FRAMES_IN_FLIGHT)

Defers a GPU-resource free until every frame that could reference it has retired.

Hand a destructor here instead of calling it: the queue holds the callable for as long as it takes the frames in flight at retirement time to complete, then runs it.

The clock is COMPLETED FRAMES, not calls. :meth:frame_completed is called from exactly one place, immediately after a frame fence has actually signalled, so a run of frames that submit nothing (a pending resize, a lost device) does not advance it.

A completed-frame count proves that the frames which referenced a resource have retired. It proves nothing about an out-of-band vkQueueSubmit issued between two frames: that needs a fence on that submission, which this queue does not take and deliberately does not offer.

This queue solves lifetime only. It has no idea what a descriptor is, and deferring a free does not make a descriptor rewrite safe.

Initialization

property depth: int

Completed frames a queued free waits for.

property pending: int

Number of queued frees. Read by tests and by the leak check.

property closed: bool

True once teardown has run. A retire on a closed queue is dropped.

retire(free: collections.abc.Callable[[], None]) None

Queue free to run once :attr:depth further frames have completed.

On a closed queue the callable is dropped with a warning: the device it would act on is being destroyed and every handle with it.

retire_handles(device: Any, **handles: Any) None

Queue the common handle shapes for destruction on device.

Accepts view, image, memory, buffer, sampler and descriptor_pool; falsy handles are skipped. Destruction order is fixed (view, image, memory, buffer, sampler, pool) so a caller cannot get it wrong.

frame_completed() None

One frame’s fence has signalled. Run whatever is now safe.

drain(*, close: bool = True) None

Run every pending free NOW, then optionally close.

The caller guarantees the device is idle. Anything a running callable retires is run in the same drain. close=False leaves the queue re-openable, which is what a secondary device wants: its every submission ends in vkQueueWaitIdle, so it drains rather than counting frames.

abandon() None

Drop every pending free WITHOUT running it, then close.

Device loss only: every handle from a lost device is invalid and every vkDestroy* against it is undefined.