Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions crates/samples/reactor/direct2d/src/device.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
//! Consolidated GPU device shared by every Direct2D sample in this app.
//!
//! A single [`SharedDevice`] is created once and shared via the [`Gpu`] context,
//! so every sample renders with the same device.
//!
//! The D2D factory is `MULTI_THREADED` because the swap-chain sample presents
//! from a worker thread while the surface-image-source sample draws on the UI
//! thread. That only serializes D2D's own calls, not raw D3D/DXGI interop; the
//! `ID2D1Multithread` locking needed to fully harden that interop is not added
//! here.

use std::ops::Deref;
use std::rc::Rc;
use std::sync::LazyLock;

use windows::Win32::Foundation::D2DERR_RECREATE_TARGET;
use windows::Win32::Graphics::Direct2D::*;
use windows::Win32::Graphics::Direct3D::*;
use windows::Win32::Graphics::Direct3D11::*;
use windows::Win32::Graphics::Dxgi::*;
use windows::core::{HRESULT, Interface, Result};
use windows_reactor::{Context, Updater};

/// The app-wide shared GPU device: the D3D11 device, the `MULTI_THREADED` D2D
/// factory and device, and the DXGI factory.
///
/// Every interface is an agile COM object, so a clone can be moved onto the
/// swap-chain sample's render thread; see [`Device::to_send`].
#[derive(Clone)]
pub struct SharedDevice {
d3d_device: ID3D11Device,
d2d_device: ID2D1Device,
dxgi_factory: IDXGIFactory2,
}

// SAFETY: every interface held here is an agile (free-threaded) COM object.
unsafe impl Send for SharedDevice {}
Comment on lines +36 to +37

impl SharedDevice {
/// Create a hardware-backed shared device.
fn new() -> Result<Self> {
let mut d3d_device: Option<ID3D11Device> = None;
unsafe {
D3D11CreateDevice(
None,
D3D_DRIVER_TYPE_HARDWARE,
None,
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
Some(&[D3D_FEATURE_LEVEL_11_0]),
D3D11_SDK_VERSION,
Some(&mut d3d_device),
None,
None,
)?;
}
let d3d_device = d3d_device.unwrap();

// MULTI_THREADED so the one D2D device works from both the UI and render
// threads.
let d2d_factory: ID2D1Factory1 =
unsafe { D2D1CreateFactory(D2D1_FACTORY_TYPE_MULTI_THREADED, None)? };

let dxgi_device: IDXGIDevice = d3d_device.cast()?;
let d2d_device = unsafe { d2d_factory.CreateDevice(&dxgi_device)? };

let dxgi_adapter = unsafe { dxgi_device.GetAdapter()? };
let dxgi_factory: IDXGIFactory2 = unsafe { dxgi_adapter.GetParent()? };

Ok(Self {
d3d_device,
d2d_device,
dxgi_factory,
})
}

/// The shared D3D11 device.
pub fn d3d_device(&self) -> &ID3D11Device {
&self.d3d_device
}

/// The shared Direct2D device. Create a per-thread device context from this.
pub fn d2d_device(&self) -> &ID2D1Device {
&self.d2d_device
}

/// The DXGI factory, for creating the composition swap chain.
pub fn dxgi_factory(&self) -> &IDXGIFactory2 {
&self.dxgi_factory
}
}

/// Reference-counted handle to a [`SharedDevice`], usable as a context value and
/// a `use_effect` dependency.
///
/// Equality is by identity (`Rc::ptr_eq`), so a recreated device compares
/// unequal to the old one, driving device-keyed dependents to rebuild.
#[derive(Clone)]
pub struct Device(Rc<SharedDevice>);

impl Device {
/// Create a new shared device.
pub fn new() -> Result<Self> {
Ok(Self(Rc::new(SharedDevice::new()?)))
}

/// A `Send` snapshot of the agile COM interfaces, for moving onto the render
/// thread. Shares the same underlying COM objects as this handle.
pub fn to_send(&self) -> SharedDevice {
(*self.0).clone()
}
}

impl Deref for Device {
type Target = SharedDevice;
fn deref(&self) -> &Self::Target {
&self.0
}
}

impl PartialEq for Device {
fn eq(&self, other: &Self) -> bool {
Rc::ptr_eq(&self.0, &other.0)
}
}

/// What every sample needs from the root: the shared [`Device`] (`None` until
/// created, and briefly during recreation) and a way to request recovery.
#[derive(Clone, PartialEq)]
Comment on lines +126 to +128
pub struct Gpu {
device: Option<Device>,
recover: Updater<u32>,
}

impl Gpu {
pub fn new(device: Option<Device>, recover: Updater<u32>) -> Self {
Self { device, recover }
}

/// The shared device, or `None` while it is being (re)created.
pub fn device(&self) -> Option<Device> {
self.device.clone()
}

/// Ask the root to recreate the shared device. Bumps a counter that re-runs
/// the root's create/recover effect; recreation is unconditional.
pub fn request_recovery(&self) {
self.recover.call(|g| g.wrapping_add(1));
}
}

/// Stable id for the GPU context. Held as a `Context<()>` so the `static` is
/// `Sync`; the real value holds `Rc`s, so [`gpu_context`] rebuilds the typed
/// [`Context`] on demand.
static GPU_KEY: LazyLock<Context<()>> = LazyLock::new(|| Context::new(()));

/// The app-wide GPU context. `None` until the root installs it.
pub fn gpu_context() -> Context<Option<Gpu>> {
Context {
default: None,
id: GPU_KEY.id,
}
}

/// Whether an `HRESULT` means the GPU device was lost and must be recreated.
/// Matches the set Win2D treats as device-lost in
/// `DeviceLostException::IsDeviceLostHResult`.
pub fn is_device_lost(hr: HRESULT) -> bool {
matches!(
hr,
DXGI_ERROR_DEVICE_HUNG
| DXGI_ERROR_DEVICE_REMOVED
| DXGI_ERROR_DEVICE_RESET
| DXGI_ERROR_DRIVER_INTERNAL_ERROR
| DXGI_ERROR_INVALID_CALL
| D2DERR_RECREATE_TARGET
)
}
1 change: 1 addition & 0 deletions crates/samples/reactor/direct2d/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

use windows_reactor::*;

mod device;
mod shell;
mod surface_image_source;
mod swap_chain;
Expand Down
63 changes: 61 additions & 2 deletions crates/samples/reactor/direct2d/src/shell.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,46 @@
//! Hub shell hosting the Direct2D samples in a `NavigationView`. Each sample is
//! a self-contained component, so its state (such as the render thread) is
//! created and torn down as the user navigates between samples.
//!
//! The shell owns the app-wide shared GPU [`Device`] and publishes it (with the
//! recovery signal) through the [`Gpu`](crate::device::Gpu) context, so every
//! sample renders with the same device.

use crate::device::{Device, Gpu, gpu_context};
use crate::surface_image_source::surface_image_source_sample;
use crate::swap_chain::swap_chain_sample;
use windows_reactor::*;

pub fn shell(cx: &mut RenderCx) -> Element {
let (selected_tag, set_selected_tag) = cx.use_state(String::from("swap-chain"));

// The single shared device for the whole app. `use_reducer` gives a
// functional updater whose closure sees the current device at apply time.
let (device, update_device) = cx.use_reducer::<Option<Device>>(None);

// Bumped on each `Gpu::request_recovery`; also seeds the create/recover
// effect, so the device is created once on mount.
let (recover_gen, bump_recover) = cx.use_reducer::<u32>(0);

// Create the device on mount and recreate it on every recovery request.
cx.use_effect(recover_gen, {
let update_device = update_device.clone();
move || {
update_device.call(|current| match Device::new() {
Ok(d) => Some(d),
Err(e) => {
eprintln!("failed to create shared device: {e}");
current
}
});
}
});

// Memoize the recovery updater so the `Gpu` context value keeps a stable
// identity across renders (the raw updater is freshly allocated each render).
let bump_recover = cx.use_memo((), move || bump_recover);
let gpu = Gpu::new(device, bump_recover);

let nav_items = vec![
NavViewItem::new("Swap Chain Panel")
.tag("swap-chain")
Expand All @@ -23,7 +55,21 @@ pub fn shell(cx: &mut RenderCx) -> Element {
_ => component(swap_chain_sample, ()),
};

NavigationView::new(nav_items, content)
// Test aid: recreate the shared device on demand, without a real GPU
// device-removal event.
let recreate_device = {
move || {
update_device.call(|current| match Device::new() {
Ok(d) => Some(d),
Err(e) => {
eprintln!("failed to recreate shared device: {e}");
current
}
});
}
};
Comment on lines +58 to +70

let nav_view = NavigationView::new(nav_items, content)
.selected_tag(&selected_tag)
.settings_visible(false)
.pane_title("Direct2D Samples")
Expand All @@ -33,5 +79,18 @@ pub fn shell(cx: &mut RenderCx) -> Element {
set_selected_tag.call(tag);
}
})
.into()
.provide(&gpu_context(), Some(gpu));

// Stack a "Recreate Device" button beneath the navigation view.
grid((
nav_view.grid_row(0),
button("Recreate Device")
.icon(SymbolGlyph::Refresh)
.on_click(recreate_device)
.margin(Thickness::uniform(12.0))
.grid_row(1),
))
.rows([GridLength::Star(1.0), GridLength::Auto])
.columns([GridLength::Star(1.0)])
.into()
}
87 changes: 36 additions & 51 deletions crates/samples/reactor/direct2d/src/surface_image_source.rs
Original file line number Diff line number Diff line change
@@ -1,52 +1,22 @@
//! Demo of displaying a `SurfaceImageSource` with the reactor `Image` widget,
//! drawing into it once with Direct2D.
//! drawing into it once with Direct2D using the app-wide shared device.

use crate::device::{Device, Gpu, gpu_context, is_device_lost};
use windows::Win32::Graphics::Direct2D::Common::*;
use windows::Win32::Graphics::Direct2D::*;
use windows::Win32::Graphics::Direct3D::*;
use windows::Win32::Graphics::Direct3D11::*;
use windows::Win32::Graphics::Dxgi::*;
use windows::core::Interface;
use windows_numerics::{Matrix3x2, Vector2};
use windows_reactor::*;

/// Surface size, in physical pixels. Also used as the element's DIP size for a
/// 1:1 mapping at 96 DPI.
const SIZE: i32 = 256;

/// Create a Direct2D device backed by a hardware D3D11 device, suitable for
/// handing to a `SurfaceImageSource`.
fn create_d2d_device() -> windows::core::Result<ID2D1Device> {
let mut d3d_device: Option<ID3D11Device> = None;
unsafe {
D3D11CreateDevice(
None,
D3D_DRIVER_TYPE_HARDWARE,
None,
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
Some(&[D3D_FEATURE_LEVEL_11_0]),
D3D11_SDK_VERSION,
Some(&mut d3d_device),
None,
None,
)?;
}
let d3d_device = d3d_device.unwrap();

let d2d_factory: ID2D1Factory1 =
unsafe { D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, None)? };
let dxgi_device: IDXGIDevice = d3d_device.cast()?;
unsafe { d2d_factory.CreateDevice(&dxgi_device) }
}

/// Create a `SurfaceImageSource`, attach a Direct2D device, and draw a single
/// static frame into it. Runs on the UI thread, as required by
/// Create a `SurfaceImageSource`, attach the shared Direct2D device, and draw a
/// single static frame into it. Runs on the UI thread, as required by
/// `SurfaceImageSource`.
fn build_surface() -> windows::core::Result<SurfaceImageSource> {
fn build_surface(device: &Device) -> windows::core::Result<SurfaceImageSource> {
let surface = SurfaceImageSource::new(SIZE, SIZE)?;

let device = create_d2d_device()?;
surface.set_device(&device)?;
surface.set_device(device.d2d_device())?;

let (context, (offset_x, offset_y)) =
surface.begin_draw::<ID2D1DeviceContext>(0, 0, SIZE, SIZE)?;
Expand Down Expand Up @@ -94,22 +64,37 @@ fn build_surface() -> windows::core::Result<SurfaceImageSource> {
/// Sample page: a static Direct2D drawing rendered into a `SurfaceImageSource`
/// and displayed with the reactor `Image` widget.
pub fn surface_image_source_sample(_: &(), cx: &mut RenderCx) -> Element {
// Create and draw the surface once; it persists across re-renders.
let surface = cx.use_ref::<Option<SurfaceImageSource>>(None);
if surface.borrow().is_none() {
match build_surface() {
Ok(sis) => surface.set(Some(sis)),
Err(e) => eprintln!("failed to build surface: {e}"),
let gpu = cx.use_context(&gpu_context());
let device = gpu.as_ref().and_then(Gpu::device);
let (surface, set_surface) = cx.use_state::<Option<SurfaceImageSource>>(None);

// (Re)build the surface whenever the shared device appears or changes (e.g.
// after recovery). On device loss, ask the root to recreate the device.
cx.use_effect(device.clone(), {
move || match device.as_ref() {
Some(dev) => match build_surface(dev) {
Ok(sis) => set_surface.call(Some(sis)),
Err(e) if is_device_lost(e.code()) => {
if let Some(gpu) = gpu.as_ref() {
gpu.request_recovery();
}
}
Err(e) => eprintln!("failed to build surface: {e}"),
},
None => set_surface.call(None),
}
}
});

vstack((
text_block("Image backed by a SurfaceImageSource:"),
Image::new(surface.get_cloned().into())
let image: Element = match surface {
Some(sis) => Image::new(sis.into())
.width(SIZE as f64)
.height(SIZE as f64),
))
.spacing(8.0)
.margin(Thickness::uniform(16.0))
.into()
.height(SIZE as f64)
.into(),
None => text_block("Creating shared device\u{2026}").into(),
};

vstack((text_block("Image backed by a SurfaceImageSource:"), image))
.spacing(8.0)
.margin(Thickness::uniform(16.0))
.into()
}
Loading
Loading