use ferroui_base::media::imaging::WriteableBitmap;
use ferroui_base::platform::surfaces::{
FuncFramebufferRenderTarget, IFramebufferPlatformSurface, IFramebufferRenderTarget, IPlatformRenderSurface,
};
use ferroui_base::platform::{AlphaFormat, ILockedFramebuffer, PixelFormat};
use ferroui_base::{PixelSize, Size, Vector};
use std::any::Any;
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError, Weak};
#[derive(Clone, Copy)]
struct FramebufferMetrics {
client_size: Size,
render_scaling: f64,
}
struct RenderedFrame {
data: Vec<u8>,
size: PixelSize,
row_bytes: i32,
dpi: Vector,
format: PixelFormat,
}
pub(crate) struct HeadlessWindowSurface {
this: Weak<HeadlessWindowSurface>,
frame_buffer_format: PixelFormat,
metrics: Mutex<FramebufferMetrics>,
last_rendered_frame: Mutex<Option<RenderedFrame>>,
}
fn guard<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(PoisonError::into_inner)
}
impl HeadlessWindowSurface {
pub(crate) fn new(frame_buffer_format: PixelFormat, client_size: Size, render_scaling: f64) -> Arc<Self> {
Arc::new_cyclic(|this| HeadlessWindowSurface {
this: this.clone(),
frame_buffer_format,
metrics: Mutex::new(FramebufferMetrics { client_size, render_scaling }),
last_rendered_frame: Mutex::new(None),
})
}
pub(crate) fn client_size(&self) -> Size {
guard(&self.metrics).client_size
}
pub(crate) fn set_client_size(&self, client_size: Size) {
guard(&self.metrics).client_size = client_size;
}
pub(crate) fn render_scaling(&self) -> f64 {
guard(&self.metrics).render_scaling
}
pub(crate) fn set_render_scaling(&self, render_scaling: f64) {
guard(&self.metrics).render_scaling = render_scaling;
}
fn lock(self: &Arc<Self>) -> Rc<dyn ILockedFramebuffer> {
let metrics = *guard(&self.metrics);
let size = PixelSize::from_size(metrics.client_size, metrics.render_scaling);
if size.width <= 0 || size.height <= 0 {
panic!("Size should be >= (1,1)");
}
let format = self.frame_buffer_format;
let bytes_per_pixel = (format.bits_per_pixel() as i32 + 7) / 8;
let row_bytes = size
.width
.checked_mul(bytes_per_pixel)
.and_then(|bytes| bytes.checked_add(3))
.map(|bytes| 4 * (bytes / 4))
.unwrap_or_else(|| panic!("framebuffer too wide: {} pixels of {format}", size.width));
let memory_size = row_bytes
.checked_mul(size.height)
.unwrap_or_else(|| panic!("framebuffer too large: {}x{} pixels of {format}", size.width, size.height));
Rc::new(HeadlessWindowFramebuffer {
surface: self.clone(),
memory: RefCell::new(Some(vec![0u8; memory_size as usize])),
size,
row_bytes,
dpi: Vector::new(96.0, 96.0) * metrics.render_scaling,
format,
})
}
pub(crate) fn get_last_rendered_frame(&self) -> Option<WriteableBitmap> {
let last_rendered_frame = guard(&self.last_rendered_frame);
let frame = last_rendered_frame.as_ref()?;
Some(WriteableBitmap::from_pixels(
frame.format,
AlphaFormat::Opaque,
&frame.data,
frame.size,
frame.dpi,
frame.row_bytes,
))
}
pub(crate) fn dispose_last_rendered_frame(&self) {
let last_rendered_frame = guard(&self.last_rendered_frame).take();
drop(last_rendered_frame);
}
}
impl IPlatformRenderSurface for HeadlessWindowSurface {
fn as_framebuffer_surface(&self) -> Option<&dyn IFramebufferPlatformSurface> {
Some(self)
}
fn as_any(&self) -> &dyn Any {
self
}
}
impl IFramebufferPlatformSurface for HeadlessWindowSurface {
fn create_framebuffer_render_target(&self) -> Rc<dyn IFramebufferRenderTarget> {
let surface = self.this.upgrade().expect("the surface is alive while it is used");
Rc::new(FuncFramebufferRenderTarget::new(move || surface.lock()))
}
}
struct HeadlessWindowFramebuffer {
surface: Arc<HeadlessWindowSurface>,
memory: RefCell<Option<Vec<u8>>>,
size: PixelSize,
row_bytes: i32,
dpi: Vector,
format: PixelFormat,
}
impl ILockedFramebuffer for HeadlessWindowFramebuffer {
fn address(&self) -> *mut u8 {
match self.memory.borrow_mut().as_mut() {
Some(memory) => memory.as_mut_ptr(),
None => std::ptr::null_mut(),
}
}
fn with_data(&self, access: &mut dyn FnMut(&mut [u8])) {
match self.memory.borrow_mut().as_mut() {
Some(memory) => access(memory),
None => access(&mut []),
}
}
fn size(&self) -> PixelSize {
self.size
}
fn row_bytes(&self) -> i32 {
self.row_bytes
}
fn dpi(&self) -> Vector {
self.dpi
}
fn format(&self) -> PixelFormat {
self.format
}
fn alpha_format(&self) -> AlphaFormat {
AlphaFormat::Premul
}
fn dispose(&self) {
let Some(data) = self.memory.borrow_mut().take() else {
return;
};
let frame =
RenderedFrame { data, size: self.size, row_bytes: self.row_bytes, dpi: self.dpi, format: self.format };
let old = guard(&self.surface.last_rendered_frame).replace(frame);
drop(old);
}
}
#[cfg(test)]
mod tests {
use super::*;
use ferroui_base::platform::{PixelFormats, RenderTargetSceneInfo};
use ferroui_base::rendering::composition::CompositionTransparencyLevel;
fn assert_send_sync<T: Send + Sync>() {}
#[test]
fn the_surface_is_shared_between_the_threads() {
assert_send_sync::<HeadlessWindowSurface>();
}
#[test]
fn a_frame_rendered_on_another_thread_becomes_the_last_rendered_frame() {
let surface = HeadlessWindowSurface::new(PixelFormats::RGBA8888, Size::new(4.0, 2.0), 2.0);
assert!(guard(&surface.last_rendered_frame).is_none());
let shared: Arc<dyn IPlatformRenderSurface> = surface.clone();
std::thread::spawn(move || {
let target = shared.as_framebuffer_surface().unwrap().create_framebuffer_render_target();
let scene_info =
RenderTargetSceneInfo::new(PixelSize::new(8, 4), 2.0, CompositionTransparencyLevel::default());
let (framebuffer, _) = target.lock(&scene_info);
assert_eq!(PixelSize::new(8, 4), framebuffer.size());
assert_eq!(32, framebuffer.row_bytes());
assert_eq!(Vector::new(192.0, 192.0), framebuffer.dpi());
framebuffer.with_data(&mut |data| data.fill(7));
framebuffer.dispose();
framebuffer.dispose();
target.dispose();
})
.join()
.unwrap();
{
let frame = guard(&surface.last_rendered_frame);
let frame = frame.as_ref().unwrap();
assert_eq!(PixelSize::new(8, 4), frame.size);
assert_eq!(vec![7u8; 32 * 4], frame.data);
}
surface.dispose_last_rendered_frame();
assert!(guard(&surface.last_rendered_frame).is_none());
}
}