forked from 1b-agent/fieldcraft-upstream
ui: ui.render draws on the window's GPU
The app keeps a capturer on its own device; ui.render draws with the viewport's pipelines there, on the CPU with the reason otherwise, and the reply names the renderer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
b655af305a
commit
6136770c1c
2 files changed
+87
-15
No files matched your search
@@ -631,12 +631,12 @@ pub fn handle(app: &mut ModelApp, ctx: &egui::Context, req: &ControlRequest) ->
|
|||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => return err(e),
|
Err(e) => return err(e),
|
||||||
};
|
};
|
||||||
let png = match app.render_png(w, h, f64::from(scale)) {
|
let (png, renderer) = match app.render_png(w, h, f64::from(scale)) {
|
||||||
Ok(png) => png,
|
Ok(drawn) => drawn,
|
||||||
Err(e) => return err(e),
|
Err(e) => return err(e),
|
||||||
};
|
};
|
||||||
match std::fs::write(path, &png) {
|
match std::fs::write(path, &png) {
|
||||||
Ok(()) => ok(json!({"path": path, "width": w, "height": h, "scale": scale, "bytes": png.len()})),
|
Ok(()) => ok(json!({"path": path, "width": w, "height": h, "scale": scale, "bytes": png.len(), "renderer": renderer.report()})),
|
||||||
Err(e) => err(format!("{path}: {e}")),
|
Err(e) => err(format!("{path}: {e}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -996,6 +996,7 @@ mod tests {
|
|||||||
let path = std::env::temp_dir().join(format!("fieldcraft-render-{}.png", std::process::id()));
|
let path = std::env::temp_dir().join(format!("fieldcraft-render-{}.png", std::process::id()));
|
||||||
let r = call(&mut app, "ui.render", json!({"path": path.to_string_lossy(), "width": 120, "height": 80}));
|
let r = call(&mut app, "ui.render", json!({"path": path.to_string_lossy(), "width": 120, "height": 80}));
|
||||||
assert_eq!(r["ok"], true, "{r}");
|
assert_eq!(r["ok"], true, "{r}");
|
||||||
|
assert_eq!(r["result"]["renderer"]["kind"], "cpu", "{r}");
|
||||||
let bytes = std::fs::read(&path).unwrap();
|
let bytes = std::fs::read(&path).unwrap();
|
||||||
assert!(bytes.starts_with(&[0x89, b'P', b'N', b'G']));
|
assert!(bytes.starts_with(&[0x89, b'P', b'N', b'G']));
|
||||||
let _ = std::fs::remove_file(&path);
|
let _ = std::fs::remove_file(&path);
|
||||||
|
|||||||
+83
-12
@@ -377,6 +377,11 @@ pub struct ModelApp {
|
|||||||
pub toolbar_rects: Vec<(&'static str, egui::Rect, toolbar::Fit)>,
|
pub toolbar_rects: Vec<(&'static str, egui::Rect, toolbar::Fit)>,
|
||||||
/// The GPU adapter, for the status bar and diagnostics.
|
/// The GPU adapter, for the status bar and diagnostics.
|
||||||
pub gpu_info: Option<String>,
|
pub gpu_info: Option<String>,
|
||||||
|
/// Draws `ui.render`'s pictures on the window's device with the viewport's pipelines; the
|
||||||
|
/// CPU rasterizer draws them without it (no GPU, the browser, or a failed set-up, kept in
|
||||||
|
/// [`App::render_png`]'s report).
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
capturer: Result<Option<fieldcraft_gpu::capture::Capturer>, String>,
|
||||||
/// Bumped whenever the model may have changed.
|
/// Bumped whenever the model may have changed.
|
||||||
revision: u64,
|
revision: u64,
|
||||||
display: (Option<u64>, DisplayMesh),
|
display: (Option<u64>, DisplayMesh),
|
||||||
@@ -515,6 +520,8 @@ impl ModelApp {
|
|||||||
icons: icons::IconCache::default(),
|
icons: icons::IconCache::default(),
|
||||||
toolbar_rects: Vec::new(),
|
toolbar_rects: Vec::new(),
|
||||||
gpu_info: None,
|
gpu_info: None,
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
capturer: Ok(None),
|
||||||
revision: 0,
|
revision: 0,
|
||||||
display: (None, DisplayMesh::default()),
|
display: (None, DisplayMesh::default()),
|
||||||
glyphs: Glyphs::default(),
|
glyphs: Glyphs::default(),
|
||||||
@@ -601,6 +608,15 @@ impl ModelApp {
|
|||||||
self.viewport.gpu = Some(gpu::install(rs, depth_bits, samples, self.viewport.gpu_errors.clone(), ctx.clone()));
|
self.viewport.gpu = Some(gpu::install(rs, depth_bits, samples, self.viewport.gpu_errors.clone(), ctx.clone()));
|
||||||
let info = rs.adapter.get_info();
|
let info = rs.adapter.get_info();
|
||||||
self.gpu_info = Some(format!("{} ({:?})", info.name.trim(), info.backend));
|
self.gpu_info = Some(format!("{} ({:?})", info.name.trim(), info.backend));
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
{
|
||||||
|
let tier = fieldcraft_gpu::select::rank(info.device_type).0;
|
||||||
|
self.capturer =
|
||||||
|
fieldcraft_gpu::capture::Capturer::on_device(&rs.adapter, rs.device.clone(), rs.queue.clone(), tier).map(Some).map_err(|e| {
|
||||||
|
log::warn!("ui.render draws on the CPU: {e}");
|
||||||
|
e.to_string()
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run a command with JSON parameters: what every panel, the control channel and scripts
|
/// Run a command with JSON parameters: what every panel, the control channel and scripts
|
||||||
@@ -1360,22 +1376,47 @@ impl ModelApp {
|
|||||||
bounds
|
bounds
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A CPU render of the viewport's scene with the current camera and the selection, as PNG
|
/// A render of the viewport's scene with the current camera and the selection, as PNG
|
||||||
/// bytes, `w` × `h` pixels at `pixels_per_point`: symbols, lines and nodes are as large as
|
/// bytes, `w` × `h` pixels at `pixels_per_point`: symbols, lines and nodes are as large as
|
||||||
/// in a window of that scale (#285).
|
/// in a window of that scale (#285). Drawn with the viewport's pipelines on the window's
|
||||||
pub fn render_png(&mut self, w: usize, h: usize, pixels_per_point: f64) -> Result<Vec<u8>, fieldcraft_render::RenderError> {
|
/// device when there is one, else (or when the GPU fails) by the CPU rasterizer, with why;
|
||||||
|
/// the second value says which.
|
||||||
|
pub fn render_png(&mut self, w: usize, h: usize, pixels_per_point: f64) -> Result<(Vec<u8>, fieldcraft_gpu::Renderer), String> {
|
||||||
let cam = self.cam;
|
let cam = self.cam;
|
||||||
let (_, overlay) = self.overlay();
|
let (_, overlay) = self.overlay();
|
||||||
self.symbols_for(h as f64, pixels_per_point);
|
self.symbols_for(h as f64, pixels_per_point);
|
||||||
self.scene();
|
self.scene();
|
||||||
if pixels_per_point == 1.0 {
|
let scaled;
|
||||||
return fieldcraft_render::render_layers_png(&self.scene.1, &[&self.symbols.1, &overlay], &cam, w, h);
|
let (scene, symbols, overlay) = if pixels_per_point == 1.0 {
|
||||||
}
|
(&self.scene.1, &self.symbols.1, &*overlay)
|
||||||
let (mut scene, mut symbols, mut overlay) = (self.scene.1.clone(), self.symbols.1.clone(), (*overlay).clone());
|
} else {
|
||||||
for s in [&mut scene, &mut symbols, &mut overlay] {
|
let (mut scene, mut symbols, mut overlay) = (self.scene.1.clone(), self.symbols.1.clone(), (*overlay).clone());
|
||||||
s.scale_marks(pixels_per_point);
|
for s in [&mut scene, &mut symbols, &mut overlay] {
|
||||||
}
|
s.scale_marks(pixels_per_point);
|
||||||
fieldcraft_render::render_layers_png(&scene, &[&symbols, &overlay], &cam, w, h)
|
}
|
||||||
|
scaled = (scene, symbols, overlay);
|
||||||
|
(&scaled.0, &scaled.1, &scaled.2)
|
||||||
|
};
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
let why = match &self.capturer {
|
||||||
|
Ok(Some(c)) => {
|
||||||
|
let (pw, ph) = (u32::try_from(w).unwrap_or(u32::MAX), u32::try_from(h).unwrap_or(u32::MAX));
|
||||||
|
match c.rgba(scene, &[symbols, overlay], &cam, pw, ph) {
|
||||||
|
Ok(px) => {
|
||||||
|
let png = fieldcraft_render::png(w, h, &px).ok_or("the picture could not be encoded")?;
|
||||||
|
return Ok((png, fieldcraft_gpu::Renderer::Gpu { adapter: c.adapter.clone(), tier: c.tier, samples: c.samples() }));
|
||||||
|
}
|
||||||
|
Err(e @ fieldcraft_gpu::GpuError::Size(_)) => return Err(e.to_string()),
|
||||||
|
Err(e) => Some(e.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None) => None,
|
||||||
|
Err(e) => Some(e.clone()),
|
||||||
|
};
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
let why = None;
|
||||||
|
let png = fieldcraft_render::render_layers_png(scene, &[symbols, overlay], &cam, w, h).map_err(|e| e.to_string())?;
|
||||||
|
Ok((png, fieldcraft_gpu::Renderer::Cpu { why }))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The geometry tags behind the drawn mesh, read again when the model changed.
|
/// The geometry tags behind the drawn mesh, read again when the model changed.
|
||||||
@@ -2670,10 +2711,40 @@ mod tests {
|
|||||||
// Framed when the viewport first draws.
|
// Framed when the viewport first draws.
|
||||||
draw(&mut app, &egui::Context::default(), 1280.0, 800.0);
|
draw(&mut app, &egui::Context::default(), 1280.0, 800.0);
|
||||||
assert!(app.framed(), "{:?}", app.cam);
|
assert!(app.framed(), "{:?}", app.cam);
|
||||||
let png = app.render_png(64, 48, 1.0).unwrap();
|
let png = app.render_png(64, 48, 1.0).unwrap().0;
|
||||||
assert!(png.starts_with(&[0x89, b'P', b'N', b'G']));
|
assert!(png.starts_with(&[0x89, b'P', b'N', b'G']));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `ui.render` draws with the viewport's pipelines on the window's device when there is one
|
||||||
|
/// (here a device the `FIELDCRAFT_GPU` policy opens, as the app's), says so, and draws on
|
||||||
|
/// the CPU with the reason when the GPU could not be set up.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
#[test]
|
||||||
|
fn renders_on_the_windows_gpu() {
|
||||||
|
use fieldcraft_gpu::{Renderer, capture::Capturer, select};
|
||||||
|
let mut app = ModelApp::new(Session::default());
|
||||||
|
app.run_script(&fieldcraft_engine::sample::script()).unwrap();
|
||||||
|
draw(&mut app, &egui::Context::default(), 1280.0, 800.0);
|
||||||
|
let size = |png: &[u8]| (u32::from_be_bytes(png[16..20].try_into().unwrap()), u32::from_be_bytes(png[20..24].try_into().unwrap()));
|
||||||
|
match Capturer::open(select::Policy::from_env().unwrap()) {
|
||||||
|
Ok(Some(c)) => {
|
||||||
|
app.capturer = Ok(Some(c));
|
||||||
|
let (png, drawn) = app.render_png(160, 100, 2.0).unwrap();
|
||||||
|
assert!(matches!(drawn, Renderer::Gpu { .. }), "{drawn:?}");
|
||||||
|
assert_eq!(size(&png), (160, 100));
|
||||||
|
let (gpu, _) = app.render_png(160, 100, 1.0).unwrap();
|
||||||
|
assert_ne!(png, gpu, "the scale changes the marks");
|
||||||
|
assert!(app.render_png(0, 100, 1.0).is_err());
|
||||||
|
}
|
||||||
|
Ok(None) | Err(_) if select::gpu_required() => panic!("FIELDCRAFT_REQUIRE_GPU=1 but no GPU"),
|
||||||
|
Ok(None) | Err(_) => eprintln!("no GPU adapter: the GPU half skipped"),
|
||||||
|
}
|
||||||
|
app.capturer = Err("no device".into());
|
||||||
|
let (png, drawn) = app.render_png(160, 100, 1.0).unwrap();
|
||||||
|
assert_eq!(drawn, Renderer::Cpu { why: Some("no device".into()) });
|
||||||
|
assert_eq!(size(&png), (160, 100));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_open_design_and_imported_surfaces_are_drawn() {
|
fn the_open_design_and_imported_surfaces_are_drawn() {
|
||||||
let dir = std::env::temp_dir().join(format!("fieldcraft-ui-design-{}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("fieldcraft-ui-design-{}", std::process::id()));
|
||||||
|
|||||||
Reference in new issue
Block a user