use core::num::NonZeroU32;
use core::time::Duration;
use std::rc::Rc;
use crate::context::{Engine, Recording, Run, Simulating};
use crate::gpu::Target;
use crate::input::{Input, Pads};
use crate::math::{UVec2, Vec2};
use crate::platform::Store;
use crate::renderer::{RenderStats, Renderer};
use crate::save::Saved;
use crate::sound::{Audio, Output};
use crate::time::FrameTime;
use crate::ui::Overlay;
use crate::{Camera, 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: G,
config: Config,
simulated: Duration,
drawn: Duration,
run: Run,
renderer: Renderer<G::Meshes, G::Skyboxes>,
audio: Audio<G::Sounds>,
input: Input,
saves: Saved,
overlay: Overlay,
device: wgpu::Device,
queue: wgpu::Queue,
color: wgpu::Texture,
target: Target,
size: UVec2,
last_camera: Camera,
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 (device, queue) = acquire_gpu()?;
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 assets = Rc::new(pollster::block_on(crate::platform::load_assets(
config.asset_sources(),
))?);
let mut renderer = pollster::block_on(Renderer::new::<G::SurfaceStyles, G::PostEffects>(
&device,
&queue,
TARGET_FORMAT,
&config,
Rc::clone(&assets),
))?;
let overlay = Overlay::offscreen(&device, OVERLAY_FORMAT, size);
let mut audio = Audio::<G::Sounds>::new(Rc::clone(&assets), Output::silent());
let input = Input::new::<G::InputActions>(
Pads::silent(),
Store::bindings(None),
config
.double_click_interval()
.unwrap_or(crate::platform::DOUBLE_CLICK_INTERVAL),
);
let saves = Saved::new(Store::saves(None));
audio.build_catalog();
renderer.build_catalog(&device, &queue);
if let Some(error) = assets.unresolved() {
return Err(error);
}
let engine = Engine::new(&device, &queue, size, &config, Camera::default());
let game = init(&mut InitContext::new(
engine,
&mut renderer,
&mut audio,
&saves,
&assets,
&overlay,
))?;
Ok(Self {
game,
run: Run::new(config.tick_interval()),
config,
simulated: Duration::ZERO,
drawn: Duration::ZERO,
renderer,
audio,
input,
saves,
overlay,
device,
queue,
color,
target,
size,
last_camera: Camera::default(),
cursor: Cursor::default(),
frame_interval: Duration::ZERO,
stale: false,
})
}
pub fn tick(&mut self) {
self.start();
let Self {
game,
config,
renderer,
audio,
input,
saves,
overlay,
device,
queue,
size,
simulated,
run,
last_camera,
..
} = self;
let dt = run.start_ticks();
*simulated += dt;
let elapsed = *simulated;
input.ticks(NonZeroU32::MIN, |ticking| {
let engine = Engine::new(device, queue, *size, config, *last_camera);
let simulating = Simulating { audio, saves, run };
game.tick(&mut simulating.tick_context(
engine,
renderer.meshes(),
ticking,
dt,
elapsed,
overlay.claims(),
));
});
}
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.start();
let Self {
game,
config,
renderer,
audio,
input,
saves,
overlay,
device,
queue,
target,
size,
simulated,
drawn,
run,
last_camera,
cursor,
frame_interval,
stale,
..
} = self;
let elapsed = (*simulated).max(*drawn);
let time = FrameTime {
dt: *frame_interval,
elapsed,
alpha: 0.0,
};
#[cfg(feature = "ui")]
overlay.set_clock(elapsed);
*cursor = overlay.frame(device, queue, |layer| {
let engine = Engine::new(device, queue, *size, config, *last_camera);
let recording = Recording {
renderer,
input,
simulating: Simulating { audio, saves, run },
};
game.frame(&mut recording.frame_context(engine, time, layer));
});
*last_camera = renderer.camera();
audio.flush(last_camera.view());
input.flush();
saves.flush();
*drawn += *frame_interval;
*stale = true;
FrameStats(renderer.render_to(device, queue, target, overlay))
}
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.input);
#[cfg(feature = "ui")]
if let Some(event) = driven.ui_event(&self.input, self.overlay.pixels_per_point()) {
self.overlay.feed(event);
}
self.stale = true;
}
fn start(&mut self) {
if core::mem::take(&mut self.stale) {
self.input.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.run.closing()
}
pub fn cursor(&self) -> Cursor {
self.cursor
}
pub fn game(&self) -> &G {
&self.game
}
pub fn game_mut(&mut self) -> &mut G {
&mut self.game
}
}
#[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}")))
}
pub(crate) fn acquire_gpu() -> Result<(wgpu::Device, wgpu::Queue), Error> {
pollster::block_on(async {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions::default())
.await
.map_err(|error| Error::msg(format!("{NO_ADAPTER}: {error}")))?;
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}")))
})
}
mod driven;
#[cfg(test)]
mod tests;