use std::sync::{Arc, Mutex, OnceLock, mpsc};
use winit::event::WindowEvent;
use winit::window::Window;
use accesskit::ActionRequest;
use teksilo_render::Renderer;
#[derive(Debug, thiserror::Error)]
#[error("Surface error: {0}")]
pub struct SurfaceRenderError(pub String);
#[derive(Debug)]
pub enum FrameOutcome {
Rendered,
Skipped,
NeedsReconfigure,
Error(SurfaceRenderError),
}
pub struct PlatformWindow {
window: Arc<Window>,
surface: wgpu::Surface<'static>,
surface_config: wgpu::SurfaceConfiguration,
renderer: Renderer,
scale_factor: f64,
a11y_adapter: Option<accesskit_winit::Adapter>,
a11y_action_rx: mpsc::Receiver<ActionRequest>,
}
#[derive(Clone)]
struct SharedGpu {
adapter: wgpu::Adapter,
device: wgpu::Device,
queue: wgpu::Queue,
}
fn shared_instance() -> &'static wgpu::Instance {
static INSTANCE: OnceLock<wgpu::Instance> = OnceLock::new();
INSTANCE
.get_or_init(|| wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()))
}
async fn shared_gpu_for(surface: &wgpu::Surface<'static>) -> SharedGpu {
static SHARED: Mutex<Option<SharedGpu>> = Mutex::new(None);
let cached = SHARED.lock().unwrap_or_else(|e| e.into_inner()).clone();
if let Some(gpu) = cached {
if !surface.get_capabilities(&gpu.adapter).formats.is_empty() {
return gpu;
}
}
let adapter = shared_instance()
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
compatible_surface: Some(surface),
force_fallback_adapter: false,
..Default::default()
})
.await
.expect("no compatible wgpu adapter available");
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("teksilo_device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
..Default::default()
})
.await
.expect("wgpu device request failed");
let gpu = SharedGpu {
adapter,
device,
queue,
};
let mut slot = SHARED.lock().unwrap_or_else(|e| e.into_inner());
if slot.is_none() {
*slot = Some(gpu.clone());
}
gpu
}
impl PlatformWindow {
async fn surface_and_renderer(
window: &Arc<Window>,
) -> (wgpu::Surface<'static>, wgpu::SurfaceConfiguration, Renderer) {
let size = window.inner_size();
let surface = shared_instance()
.create_surface(window.clone())
.expect("wgpu surface creation failed for the platform window");
let gpu = shared_gpu_for(&surface).await;
let surface_caps = surface.get_capabilities(&gpu.adapter);
let surface_format = surface_caps
.formats
.iter()
.find(|f| f.is_srgb())
.copied()
.or_else(|| surface_caps.formats.first().copied())
.unwrap_or(wgpu::TextureFormat::Rgba8UnormSrgb);
let surface_config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: size.width.max(1),
height: size.height.max(1),
present_mode: wgpu::PresentMode::Fifo,
alpha_mode: surface_caps
.alpha_modes
.first()
.copied()
.unwrap_or(wgpu::CompositeAlphaMode::Auto),
view_formats: vec![],
desired_maximum_frame_latency: 2,
color_space: wgpu::SurfaceColorSpace::Auto,
};
surface.configure(&gpu.device, &surface_config);
let renderer = Renderer::new(gpu.device, gpu.queue, surface_format);
(surface, surface_config, renderer)
}
pub async fn new_with_a11y(
window: Window,
event_loop: &winit::event_loop::ActiveEventLoop,
) -> Self {
let window = Arc::new(window);
let scale_factor = window.scale_factor();
let (surface, surface_config, renderer) = Self::surface_and_renderer(&window).await;
let (action_tx, action_rx) = mpsc::channel();
let a11y_adapter = accesskit_winit::Adapter::with_direct_handlers(
event_loop,
&window,
TeksiloActivationHandler,
TeksiloActionHandler { tx: action_tx },
TeksiloDeactivationHandler,
);
window.set_visible(true);
Self {
window,
surface,
surface_config,
renderer,
scale_factor,
a11y_adapter: Some(a11y_adapter),
a11y_action_rx: action_rx,
}
}
pub async fn new(window: Window) -> Self {
let window = Arc::new(window);
let scale_factor = window.scale_factor();
let (surface, surface_config, renderer) = Self::surface_and_renderer(&window).await;
let (_action_tx, action_rx) = mpsc::channel();
Self {
window,
surface,
surface_config,
renderer,
scale_factor,
a11y_adapter: None,
a11y_action_rx: action_rx,
}
}
pub fn window(&self) -> &Window {
&self.window
}
pub fn window_arc(&self) -> Arc<Window> {
self.window.clone()
}
pub fn renderer(&self) -> &Renderer {
&self.renderer
}
pub fn renderer_mut(&mut self) -> &mut Renderer {
&mut self.renderer
}
pub fn scale_factor(&self) -> f64 {
self.scale_factor
}
pub fn set_scale_factor(&mut self, factor: f64) {
self.scale_factor = factor;
}
pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
if new_size.width > 0 && new_size.height > 0 {
self.surface_config.width = new_size.width;
self.surface_config.height = new_size.height;
self.surface
.configure(self.renderer.device(), &self.surface_config);
}
}
pub fn surface_size(&self) -> (u32, u32) {
(self.surface_config.width, self.surface_config.height)
}
pub fn reconfigure_surface(&mut self) {
self.surface
.configure(self.renderer.device(), &self.surface_config);
}
pub fn render_frame(
&mut self,
frame: &teksilo_canvas::RenderFrame,
clear_color: [f32; 4],
) -> FrameOutcome {
let current = self.surface.get_current_texture();
let output = match current {
wgpu::CurrentSurfaceTexture::Success(tex)
| wgpu::CurrentSurfaceTexture::Suboptimal(tex) => tex,
wgpu::CurrentSurfaceTexture::Occluded | wgpu::CurrentSurfaceTexture::Timeout => {
return FrameOutcome::Skipped;
}
wgpu::CurrentSurfaceTexture::Outdated | wgpu::CurrentSurfaceTexture::Lost => {
return FrameOutcome::NeedsReconfigure;
}
other => return FrameOutcome::Error(SurfaceRenderError(format!("{other:?}"))),
};
let view = output
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let (w, h) = self.surface_size();
self.renderer
.render(frame, &view, self.scale_factor as f32, w, h, clear_color);
self.renderer.queue().present(output);
FrameOutcome::Rendered
}
pub fn capture_offscreen(
&mut self,
frame: &teksilo_canvas::RenderFrame,
clear_color: [f32; 4],
crop: Option<teksilo_canvas::Rect>,
) -> (Vec<u8>, u32, u32) {
fn crop_rgba(
src: &[u8],
w: u32,
h: u32,
rect: teksilo_canvas::Rect,
) -> (Vec<u8>, u32, u32) {
let x0 = (rect.x.floor().max(0.0) as u32).min(w);
let y0 = (rect.y.floor().max(0.0) as u32).min(h);
let x1 = ((rect.x + rect.width).ceil().max(0.0) as u32).min(w);
let y1 = ((rect.y + rect.height).ceil().max(0.0) as u32).min(h);
if x1 <= x0 || y1 <= y0 {
return (Vec::new(), 0, 0);
}
let cw = x1 - x0;
let ch = y1 - y0;
let mut out = Vec::with_capacity((cw * ch * 4) as usize);
for y in y0..y1 {
let row_start = ((y * w + x0) * 4) as usize;
let row_end = row_start + (cw * 4) as usize;
out.extend_from_slice(&src[row_start..row_end]);
}
(out, cw, ch)
}
let (w, h) = self.surface_size();
let format = self.surface_config.format;
debug_assert!(
matches!(
format,
wgpu::TextureFormat::Rgba8Unorm
| wgpu::TextureFormat::Rgba8UnormSrgb
| wgpu::TextureFormat::Bgra8Unorm
| wgpu::TextureFormat::Bgra8UnormSrgb
),
"capture_offscreen: unsupported surface format {format:?} (expected 8-bit RGBA/BGRA)"
);
let texture = self
.renderer
.device()
.create_texture(&wgpu::TextureDescriptor {
label: Some("teksilo-automation capture"),
size: wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
self.renderer
.render(frame, &view, self.scale_factor as f32, w, h, clear_color);
let mut bytes = teksilo_render::test_support::read_texture_rgba(
self.renderer.device(),
self.renderer.queue(),
&texture,
w,
h,
);
if matches!(
format,
wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb
) {
for px in bytes.as_chunks_mut::<4>().0 {
px.swap(0, 2);
}
}
match crop {
Some(rect) => crop_rgba(&bytes, w, h, rect),
None => (bytes, w, h),
}
}
pub fn request_redraw(&self) {
self.window.request_redraw();
}
pub fn update_accessibility(&mut self, update: accesskit::TreeUpdate) {
if let Some(adapter) = &mut self.a11y_adapter {
adapter.update_if_active(|| update);
}
}
pub fn process_accessibility_event(&mut self, event: &WindowEvent) {
if let Some(adapter) = &mut self.a11y_adapter {
adapter.process_event(&self.window, event);
}
}
pub fn drain_accessibility_actions(&self) -> Vec<ActionRequest> {
let mut actions = Vec::new();
while let Ok(req) = self.a11y_action_rx.try_recv() {
actions.push(req);
}
actions
}
}
struct TeksiloActivationHandler;
impl accesskit::ActivationHandler for TeksiloActivationHandler {
fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
let root = accesskit::Node::new(accesskit::Role::Window);
Some(accesskit::TreeUpdate {
nodes: vec![(accesskit::NodeId(0), root)],
tree: Some(accesskit::TreeInfo::new(accesskit::NodeId(0))),
tree_id: accesskit::TreeId::ROOT,
focus: accesskit::NodeId(0),
})
}
}
struct TeksiloActionHandler {
tx: mpsc::Sender<ActionRequest>,
}
impl accesskit::ActionHandler for TeksiloActionHandler {
fn do_action(&mut self, request: ActionRequest) {
let _ = self.tx.send(request);
}
}
struct TeksiloDeactivationHandler;
impl accesskit::DeactivationHandler for TeksiloDeactivationHandler {
fn deactivate_accessibility(&mut self) {
}
}