use core::num::NonZeroU32;
use core::time::Duration;
use crate::gpu::{FaultSlot, Target};
use crate::input::{Controls, Devices, Pads};
use crate::math::{UVec2, Vec2};
use crate::platform::threads::{Commands, GameThread, Kept, Stated, Workers};
use crate::renderer::{RenderStats, Renderer};
use crate::sound::{Output, SoundOutput};
use crate::time::FrameTime;
use crate::ui::Painter;
use crate::{Config, Cursor, Error, Game, InitContext, PointerDelta, WheelDelta};
use driven::Driven;
pub use crate::input::Switch;
const TARGET_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb;
pub(crate) const NO_ADAPTER: &str = "no usable graphics adapter";
const OVERLAY_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
pub struct Session<G: Game> {
game: GameThread<G>,
simulated: Duration,
drawn: Duration,
devices: Devices,
renderer: Renderer,
painter: Painter,
output: SoundOutput,
device: wgpu::Device,
queue: wgpu::Queue,
faults: KeptFault,
color: wgpu::Texture,
target: Target,
size: UVec2,
cursor: Cursor,
frame_interval: Duration,
stale: bool,
}
impl<G: Game> Session<G> {
pub fn new(
config: Config,
size: UVec2,
init: impl FnOnce(&mut InitContext<'_, G>) -> Result<G, Error>,
) -> Result<Self, Error> {
if size.x == 0 || size.y == 0 {
return Err(Error::msg(format!(
"a headless target needs a non-zero size, got {}x{}",
size.x, size.y
)));
}
crate::platform::install_diagnostics();
let Windowless {
device,
queue,
faults,
} = Windowless::acquired()?;
let color = device.create_texture(&wgpu::TextureDescriptor {
label: Some("mirage-engine headless target"),
size: wgpu::Extent3d {
width: size.x,
height: size.y,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: TARGET_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[OVERLAY_FORMAT],
});
let view = |format| {
color.create_view(&wgpu::TextureViewDescriptor {
format: Some(format),
..Default::default()
})
};
let target = Target::new(view(TARGET_FORMAT), view(OVERLAY_FORMAT), size);
let files = pollster::block_on(crate::platform::read_sources(config.asset_sources()))?;
let renderer = pollster::block_on(Renderer::new(
&device,
&queue,
TARGET_FORMAT,
&config,
crate::surface_style::Declarations::of::<G::SurfaceStyles>(),
crate::post_effect::Declarations::of::<G::PostEffects>(),
))?;
let painter = Painter::offscreen(&device, OVERLAY_FORMAT, size);
let output = SoundOutput::new(Output::silent());
let devices = Devices::new(
Pads::silent(),
config
.double_click_interval()
.unwrap_or(crate::platform::DOUBLE_CLICK_INTERVAL),
);
let kept = Kept {
bindings: None,
saves: None,
window_size: size,
mix_rate: output.rate(),
workers: Workers::here_again(),
};
let game = GameThread::start(config, files, kept, init)?;
Ok(Self {
game,
simulated: Duration::ZERO,
drawn: Duration::ZERO,
devices,
renderer,
painter,
output,
device,
queue,
faults: KeptFault::over(faults),
color,
target,
size,
cursor: Cursor::default(),
frame_interval: Duration::ZERO,
stale: false,
})
}
pub fn tick(&mut self) {
let controls = self.close_snapshot();
self.game.take(self.stated(), controls);
let dt = self.game.start_ticks();
self.simulated += dt;
self.game.ticks(NonZeroU32::MIN, dt, self.simulated);
}
pub fn with_frame_interval(mut self, interval: Duration) -> Self {
self.frame_interval = interval;
self
}
pub fn set_frame_interval(&mut self, interval: Duration) {
self.frame_interval = interval;
}
pub fn step(&mut self) -> FrameStats {
self.faults.read();
let elapsed = self.simulated.max(self.drawn);
#[cfg(feature = "ui")]
self.painter.set_clock(elapsed);
let controls = self.close_snapshot();
self.game.take(self.stated(), controls);
let time = FrameTime {
dt: self.frame_interval,
elapsed,
alpha: 0.0,
};
let (handed, commands) = self.game.frame(time, self.painter.take_input());
let Commands {
meshes,
skies,
ui,
sound,
..
} = commands;
self.renderer.take(
&self.device,
&self.queue,
meshes,
skies,
ui,
&mut self.painter,
);
self.output.play(sound);
self.cursor = handed.cursor;
self.painter.keep(handed.ui, handed.cursor);
self.drawn += self.frame_interval;
self.stale = true;
FrameStats(self.renderer.render_to(
&self.device,
&self.queue,
&self.target,
&handed.draws,
&mut self.painter,
))
}
pub fn set_pointer(&mut self, at: Vec2) {
self.feed(Driven::Pointed(at));
}
pub fn press(&mut self, control: impl Into<Switch>) {
self.feed(Driven::Switched(control.into(), true));
}
pub fn release(&mut self, control: impl Into<Switch>) {
self.feed(Driven::Switched(control.into(), false));
}
pub fn pointer_delta(&mut self, lane: PointerDelta, pixels: f32) {
self.feed(Driven::Moved(lane, pixels));
}
pub fn wheel_delta(&mut self, lane: WheelDelta, notches: f32) {
self.feed(Driven::Turned(lane, notches));
}
#[cfg(feature = "ui")]
pub fn type_text(&mut self, text: &str) {
self.feed(Driven::Typed(text.to_owned()));
}
fn feed(&mut self, driven: Driven) {
driven.drive(&mut self.devices);
#[cfg(feature = "ui")]
if let Some(event) = driven.ui_event(&self.devices, self.painter.pixels_per_point()) {
self.painter.feed(event);
}
self.stale = true;
}
fn stated(&self) -> Stated {
Stated {
window_size: self.size,
sound_unlocked: self.output.unlocked(),
}
}
fn close_snapshot(&mut self) -> Option<Controls> {
core::mem::take(&mut self.stale)
.then(|| self.devices.sample(self.simulated.max(self.drawn)))
}
pub fn wait_for_gpu(&self) -> Result<(), Error> {
wait_for_gpu(&self.device)
}
pub fn pixels(&self) -> Result<Vec<u8>, Error> {
let row = self.size.x * 4;
let padded_row = row.next_multiple_of(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
let readback = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("mirage-engine headless readback"),
size: u64::from(padded_row) * u64::from(self.size.y),
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("mirage-engine headless readback"),
});
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &self.color,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(padded_row),
rows_per_image: Some(self.size.y),
},
},
wgpu::Extent3d {
width: self.size.x,
height: self.size.y,
depth_or_array_layers: 1,
},
);
self.queue.submit([encoder.finish()]);
readback.slice(..).map_async(wgpu::MapMode::Read, |_| {});
wait_for_gpu(&self.device)?;
let mapped = readback.slice(..).get_mapped_range();
let pixels = mapped
.chunks(padded_row as usize)
.flat_map(|padded| &padded[..row as usize])
.copied()
.collect();
drop(mapped);
readback.unmap();
Ok(pixels)
}
pub fn size(&self) -> UVec2 {
self.size
}
pub fn closed(&self) -> bool {
self.game.closing()
}
pub fn fault(&self) -> Option<Error> {
self.faults.error()
}
pub fn cursor(&self) -> Cursor {
self.cursor
}
pub fn game(&self) -> &G {
self.game.game()
}
pub fn game_mut(&mut self) -> &mut G {
self.game.game_mut()
}
}
#[derive(Clone, Copy, Debug)]
pub struct FrameStats(RenderStats);
impl FrameStats {
pub fn draw_calls(&self) -> u32 {
self.0.draw_calls
}
pub fn instances(&self) -> u32 {
self.0.instances
}
}
pub(crate) fn wait_for_gpu(device: &wgpu::Device) -> Result<(), Error> {
device
.poll(wgpu::PollType::wait_indefinitely())
.map(|_| ())
.map_err(|error| Error::msg(format!("the GPU never finished the work: {error}")))
}
struct Windowless {
device: wgpu::Device,
queue: wgpu::Queue,
faults: FaultSlot,
}
impl Windowless {
fn acquired() -> Result<Self, Error> {
pollster::block_on(async {
let instance =
wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
..Default::default()
})
.await
.map_err(|error| Error::msg(format!("{NO_ADAPTER}: {error}")))?;
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("mirage-engine headless"),
..Default::default()
})
.await
.map_err(|error| {
Error::msg(format!("the graphics adapter refused a device: {error}"))
})?;
let faults = FaultSlot::watching(&device);
Ok(Self {
device,
queue,
faults,
})
})
}
}
struct KeptFault {
slot: FaultSlot,
kept: Option<Error>,
}
impl KeptFault {
fn over(slot: FaultSlot) -> Self {
Self { slot, kept: None }
}
fn read(&mut self) {
let Some(fault) = self.slot.taken() else {
return;
};
self.kept
.get_or_insert_with(|| Error::msg(fault.to_string()));
}
fn error(&self) -> Option<Error> {
self.kept.clone()
}
}
mod driven;
#[cfg(test)]
mod tests;