use crate::backend::ContextHandle;
use crate::device::Device;
use crate::error::GoldyError;
use crate::parcel::BytesByKind;
use crate::timeline::{is_ready, ReferenceTable, TimelineValue};
use crate::transient_pool::TransientPool;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
pub struct Context {
pub(crate) inner: Arc<ContextInner>,
}
pub(crate) struct ContextInner {
device: Device,
handle: ContextHandle,
deletion_flush: Option<Arc<dyn crate::backend::ContextDeferredDeletionFlush>>,
gpu_progress: Option<Arc<dyn crate::backend::ContextGpuProgress>>,
reclamation_scope: Option<Arc<dyn crate::backend::ContextReclamationScope>>,
submit_session: Option<Arc<dyn crate::backend::ContextSubmitSession>>,
high_water_timeline: AtomicU64,
transient_pool: Mutex<TransientPool>,
}
impl Clone for Context {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
impl std::fmt::Debug for Context {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Context").finish_non_exhaustive()
}
}
impl Drop for ContextInner {
fn drop(&mut self) {
if let Ok(mut pool_guard) = self.transient_pool.lock() {
*pool_guard = TransientPool::new();
}
self.deletion_flush.take();
self.gpu_progress.take();
self.reclamation_scope.take();
self.submit_session.take();
crate::backend::destroy_context(&self.device.inner.backend, self.handle);
}
}
impl Context {
pub(crate) fn new(device: Device) -> Result<Self, GoldyError> {
let handle = {
let mut backend = device.inner.backend.lock().unwrap();
backend
.create_context(device.inner.handle)
.map_err(GoldyError::Backend)?
};
let (deletion_flush, reclamation_scope) = {
let backend = device.inner.backend.lock().unwrap();
let deletion_flush = backend
.clone_context_deletion_flush(handle)
.ok_or_else(|| GoldyError::Backend(anyhow::anyhow!("missing context deletion flush")))?;
let reclamation_scope = backend.clone_context_reclamation_scope(handle);
(deletion_flush, reclamation_scope)
};
let (submit_session, gpu_progress) = {
let backend = device.inner.backend.lock().unwrap();
(
backend.clone_context_submit_session(handle, Arc::clone(&device.inner.backend)),
backend.clone_context_gpu_progress(handle),
)
};
Ok(Self {
inner: Arc::new(ContextInner {
device,
handle,
deletion_flush: Some(deletion_flush),
gpu_progress,
reclamation_scope: Some(reclamation_scope),
submit_session: Some(submit_session),
high_water_timeline: AtomicU64::new(0),
transient_pool: Mutex::new(TransientPool::new()),
}),
})
}
pub fn device(&self) -> &Device {
&self.inner.device
}
pub(crate) fn backend_handle(&self) -> ContextHandle {
self.inner.handle
}
pub(crate) fn submit_session(&self) -> &dyn crate::backend::ContextSubmitSession {
self.inner.submit_session.as_ref().expect("submit session").as_ref()
}
#[doc(hidden)]
pub fn test_backend_handle(&self) -> ContextHandle {
self.backend_handle()
}
pub(crate) fn with_transient_pool<F, R>(&self, f: F) -> R
where
F: FnOnce(&mut TransientPool) -> R,
{
let mut pool = self.inner.transient_pool.lock().unwrap();
f(&mut pool)
}
pub fn acquire_transient_texture(
&self,
width: u32,
height: u32,
format: crate::types::TextureFormat,
access: crate::types::TextureKind,
flags: crate::types::TextureFlags,
) -> anyhow::Result<crate::Texture> {
self.with_transient_pool(|pool| pool.acquire_texture(self, width, height, format, access, flags))
}
pub fn return_transient_texture(&self, texture: crate::Texture) {
let ready_after = texture.last_referenced();
self.with_transient_pool(|pool| pool.return_texture(texture, ready_after));
}
pub fn clear_transient_textures(&self) {
self.with_transient_pool(|pool| pool.clear_textures());
}
pub fn acquire_transient_buffer(
&self,
size: u64,
kind: crate::types::BufferKind,
flags: crate::types::BufferFlags,
element_stride: Option<u32>,
) -> anyhow::Result<crate::parcel::Buffer> {
self.with_transient_pool(|pool| pool.acquire_whole_buffer(self, size, kind, flags, element_stride))
}
pub fn return_transient_buffer(&self, buf: crate::parcel::Buffer) {
let ready_after = buf.last_referenced();
match buf.into_transient_parcel() {
Ok(parcel) => {
self.with_transient_pool(|pool| pool.return_buffer_parcel(parcel, ready_after));
}
Err(e) => {
tracing::warn!("return_transient_buffer: dropping non-binneable buffer: {e}");
}
}
}
pub fn transient_outstanding_bytes(&self) -> BytesByKind {
self.with_transient_pool(|pool| pool.outstanding_bytes())
}
pub fn transient_buffer_alloc_count(&self) -> usize {
self.with_transient_pool(|pool| pool.buffer_alloc_count())
}
pub fn transient_texture_alloc_count(&self) -> usize {
self.with_transient_pool(|pool| pool.texture_alloc_count())
}
pub(crate) fn classify(&self, e: anyhow::Error) -> GoldyError {
if self.device().is_device_lost() {
return GoldyError::DeviceLost;
}
GoldyError::Backend(e)
}
pub(crate) fn gpu_progress(&self) -> TimelineValue {
let _tz = crate::tracy_zone!("context.gpu_progress");
let _query = crate::tracy_zone!("context.gpu_progress.query");
if let Some(progress) = &self.inner.gpu_progress {
return progress.gpu_progress();
}
self.inner
.device
.inner
.backend
.lock()
.unwrap()
.gpu_progress(self.inner.handle)
}
pub(crate) fn wait_until(&self, value: TimelineValue) -> Result<(), GoldyError> {
self.wait_until_context(self.inner.handle, value)
}
fn wait_until_context(&self, ctx: ContextHandle, value: TimelineValue) -> Result<(), GoldyError> {
let _tz = crate::tracy_zone!("context.wait_until");
let progress = self.gpu_progress();
let already_complete = if ctx == self.inner.handle {
progress >= value
} else {
self.inner.device.context_gpu_progress(ctx).is_some_and(|p| p >= value)
};
let backend_mutex = &self.inner.device.inner.backend;
if !already_complete {
let submission_wait = {
let _lock = crate::tracy_zone!("context.wait_until.lock");
let backend = backend_mutex.lock().unwrap();
backend
.take_timeline_submission_epoch_wait(ctx, value)
.map_err(|e| self.classify(e))?
};
if let Some(wait) = submission_wait {
let _sw = crate::tracy_zone!("context.wait_until.submission_worker");
wait.wait().map_err(|e| self.classify(e))?;
}
let blocking = {
let _lock = crate::tracy_zone!("context.wait_until.lock");
let backend = backend_mutex.lock().unwrap();
let _prepare = crate::tracy_zone!("context.wait_until.prepare");
backend
.take_timeline_blocking_wait(ctx, value)
.map_err(|e| self.classify(e))?
};
if let Some(wait) = blocking {
let _block = crate::tracy_zone!("context.wait_until.block");
wait.block().map_err(|e| self.classify(e))?;
}
}
{
let _lock = crate::tracy_zone!("context.wait_until.lock");
let mut backend = backend_mutex.lock().unwrap();
let _finish = crate::tracy_zone!("context.wait_until.finish");
backend.finish_timeline_wait(ctx, value).map_err(|e| {
drop(backend);
self.classify(e)
})?;
}
Ok(())
}
pub(crate) fn wait_until_timeout(&self, value: TimelineValue, timeout_ms: u32) -> Result<(), GoldyError> {
let ctx = self.inner.handle;
let already_complete = self.gpu_progress() >= value;
let backend_mutex = &self.inner.device.inner.backend;
if !already_complete {
let submission_wait = {
let _lock = crate::tracy_zone!("context.wait_until.lock");
let backend = backend_mutex.lock().unwrap();
backend
.take_timeline_submission_epoch_wait(ctx, value)
.map_err(|e| self.classify(e))?
};
if let Some(wait) = submission_wait {
let _sw = crate::tracy_zone!("context.wait_until.submission_worker");
wait.wait().map_err(|e| self.classify(e))?;
}
let blocking = {
let _lock = crate::tracy_zone!("context.wait_until.lock");
let backend = backend_mutex.lock().unwrap();
backend
.take_timeline_blocking_wait(ctx, value)
.map_err(|e| self.classify(e))?
};
if let Some(wait) = blocking {
let _block = crate::tracy_zone!("context.wait_until.block");
if !wait.block_timeout(timeout_ms).map_err(|e| self.classify(e))? {
return Err(GoldyError::SubmitTimeout);
}
}
}
{
let _lock = crate::tracy_zone!("context.wait_until.lock");
let mut backend = backend_mutex.lock().unwrap();
let _finish = crate::tracy_zone!("context.wait_until.finish");
backend.finish_timeline_wait(ctx, value).map_err(|e| {
drop(backend);
self.classify(e)
})
}
}
pub fn wait_until_idle(&self) -> Result<(), GoldyError> {
let hw = self.high_water_timeline();
if hw == 0 {
return Ok(());
}
self.wait_until(hw)
}
pub(crate) fn high_water_timeline(&self) -> TimelineValue {
self.inner.high_water_timeline.load(Ordering::Relaxed)
}
pub(crate) fn advance_high_water_timeline(&self, tv: TimelineValue) {
self.inner.high_water_timeline.fetch_max(tv, Ordering::Relaxed);
}
pub(crate) fn poll_signals_queued(&self) -> Vec<crate::signal::QueuedSignal> {
let progress = self.gpu_progress();
let mut backend = self.inner.device.inner.backend.lock().unwrap();
backend.poll_signals(self.inner.handle, progress)
}
pub fn poll_signals(&self) -> Vec<crate::signal::Signal> {
self.poll_signals_queued()
.into_iter()
.filter_map(|s| match s {
crate::signal::QueuedSignal::Client(c) => Some(c),
crate::signal::QueuedSignal::BoundaryCrossed { .. } => None,
})
.collect()
}
pub fn poll_signals_and_service(&self) -> Vec<crate::signal::Signal> {
let _tz = crate::tracy_zone!("context.poll_signals_and_service");
let queued = self.poll_signals_queued();
let latest_boundary = queued.iter().fold(None, |latest, signal| match signal {
crate::signal::QueuedSignal::BoundaryCrossed { epoch } => Some(latest.unwrap_or(0).max(*epoch)),
_ => latest,
});
if let Some(epoch) = latest_boundary {
self.boundary_crossed(epoch);
}
queued
.into_iter()
.filter_map(|s| match s {
crate::signal::QueuedSignal::Client(c) => Some(c),
crate::signal::QueuedSignal::BoundaryCrossed { .. } => None,
})
.collect()
}
pub(crate) fn boundary_crossed(&self, epoch: TimelineValue) {
self.boundary_crossed_inner(epoch, self.device().timeline_retired());
}
fn boundary_crossed_inner(&self, epoch: TimelineValue, vram_retire: TimelineValue) {
let _tz = crate::tracy_zone!("context.boundary_crossed");
{
let _tz = crate::tracy_zone!("context.boundary_crossed.flush_pre");
self.inner.deletion_flush.as_ref().expect("deletion flush").flush();
}
{
let _tz = crate::tracy_zone!("context.boundary_crossed.reclaim");
self.inner
.reclamation_scope
.as_ref()
.expect("reclamation scope")
.set_epoch(Some(epoch));
}
{
let _tz = crate::tracy_zone!("context.boundary_crossed.drain_vram");
self.device().vram_allocator().boundary_crossed(vram_retire);
}
{
let _tz = crate::tracy_zone!("context.boundary_crossed.drain_transient_pool");
self.with_transient_pool(|pool| pool.drain_ready(self));
}
{
let _tz = crate::tracy_zone!("context.boundary_crossed.flush_post");
self.inner
.reclamation_scope
.as_ref()
.expect("reclamation scope")
.set_epoch(None);
self.inner.deletion_flush.as_ref().expect("deletion flush").flush();
}
}
pub fn flush_deferred_deletions(&self) {
let _tz = crate::tracy_zone!("context.flush_deferred_deletions");
let progress = self.gpu_progress();
self.boundary_crossed_inner(progress, progress);
}
pub fn has_deferred_payloads(&self) -> bool {
self.device().vram_allocator().has_deferred_payloads()
}
pub fn defer_release(&self, epoch: TimelineValue, payload: crate::vram_allocator::DeferredPayload) {
self.device().vram_allocator().defer_release(epoch, payload);
}
#[cfg(test)]
pub(crate) fn defer_until<T: Send + 'static>(&self, epoch: TimelineValue, resource: T) {
let mut payload = crate::vram_allocator::DeferredPayload::new();
payload.push(resource);
self.device().vram_allocator().defer_release(epoch, payload);
}
#[doc(hidden)]
pub fn deferred_deletion_pending_count(&self) -> usize {
let backend = self.inner.device.inner.backend.lock().unwrap();
backend.deferred_deletion_pending_count(self.inner.handle)
}
#[doc(hidden)]
pub fn in_flight_command_buffer_count(&self) -> usize {
let backend = self.inner.device.inner.backend.lock().unwrap();
backend.in_flight_command_buffer_count(self.inner.handle)
}
pub(crate) fn parcel_ready(&self, refs: &ReferenceTable) -> bool {
if refs.is_empty() {
return true;
}
let device = &self.inner.device;
let mut progress = HashMap::with_capacity(refs.len());
for ctx in refs.keys() {
let p = device
.context_gpu_progress(ctx)
.unwrap_or(crate::timeline::CONTEXT_DESTROYED_PROGRESS);
progress.insert(ctx, p);
}
is_ready(refs, &progress)
}
pub fn try_resubmit_retained(&self, key: u64) -> Result<Option<TimelineValue>, GoldyError> {
if crate::validation_env::retained_cb_reuse_disabled() {
return Ok(None);
}
let result = self
.submit_session()
.try_resubmit_retained(self.inner.handle, key, None)
.map_err(|e| self.classify(e))?;
if let Some(tv) = result {
self.inner.high_water_timeline.fetch_max(tv, Ordering::Relaxed);
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use crate::backend::mock::MockBackend;
use crate::device::Device;
use crate::test_support::scheme_advance_timeline;
use std::sync::Arc;
fn test_device() -> Device {
Device::from_backend(Box::new(MockBackend::new())).unwrap()
}
#[test]
fn device_outlives_context() {
let device = test_device();
let ctx = device.create_context().unwrap();
assert_eq!(Arc::strong_count(&device.inner), 2);
drop(device);
assert_eq!(ctx.gpu_progress(), 0);
assert_eq!(Arc::strong_count(&ctx.device().inner), 1);
}
#[test]
fn device_inner_dropped_only_after_context() {
let device = test_device();
let weak = Arc::downgrade(&device.inner);
let ctx = device.create_context().unwrap();
drop(device);
assert!(weak.upgrade().is_some());
drop(ctx);
assert!(weak.upgrade().is_none());
}
#[test]
fn adapter_outlives_device() {
let device = test_device();
let adapter = device.adapter().clone();
let weak = Arc::downgrade(&adapter.inner);
drop(device);
assert!(weak.upgrade().is_some());
}
#[test]
fn context_wait_until_after_scheme_submit() {
let device = test_device();
let ctx = device.create_context().unwrap();
let tv = scheme_advance_timeline(&ctx);
ctx.wait_until(tv).unwrap();
assert!(ctx.gpu_progress() >= tv);
}
#[test]
fn high_water_timeline_starts_at_zero() {
let device = test_device();
let ctx = device.create_context().unwrap();
assert_eq!(ctx.high_water_timeline(), 0);
}
#[test]
fn high_water_timeline_advances_after_scheme_submit() {
let device = test_device();
let ctx = device.create_context().unwrap();
assert_eq!(ctx.high_water_timeline(), 0);
let tv = scheme_advance_timeline(&ctx);
assert!(tv > 0);
assert_eq!(ctx.high_water_timeline(), tv);
let tv2 = scheme_advance_timeline(&ctx);
assert!(tv2 > tv);
assert_eq!(ctx.high_water_timeline(), tv2);
}
}