mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-29 12:37:08 -06:00
M13.30: web depth and polish
- GPU in browser job workers: each job worker opens its own WebGPU device (deferred readbacks); the Render Queue export and the analyses' frame loops are futures (render::passes, offload::run_request_async) whose frames render in passes, awaiting the device between them. GPU particles and Advanced 3D read back under keys too. workers.jobs reports passes and readbacks; the smoke test measures them. - Layer buffers in the browser's OPFS disk cache: frame workers back their layer caches with a PrefetchStore; the page plans what to fetch before a request from recent misses (LayerPrefetch over the shared DiskIndex). - Templates embed footage (default on, size-capped with a warning) and creating from a template extracts it next to the new project / into browser storage. - ICC LUT profiles (lut8/lut16/lutAtoB/lutBtoA, from the public ICC spec) in Simulate Output > My Custom RGB, baked into a 3D LUT for the viewer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d39c0e818c
commit
f3715aac32
@@ -13,13 +13,18 @@
|
||||
//! - **Reads** are asynchronous: a frame whose key is indexed is read and decoded here instead
|
||||
//! of rendering it (`crate::frames`); a damaged or missing file is a miss and is forgotten.
|
||||
//!
|
||||
//! Only frames are cached on disk in the browser (the desktop also keeps slow layer buffers):
|
||||
//! a layer lookup happens in the middle of a synchronous render, where the browser's file
|
||||
//! system can't be read.
|
||||
//! - **Layer buffers** too (`layers/<32 hex>.ecc`): a layer lookup happens in the middle of a
|
||||
//! synchronous render, where the browser's file system can't be read, so the frame workers
|
||||
//! back their layer caches with a `PrefetchStore`. They report each frame's layer-cache
|
||||
//! misses; before the next request the page names the recent misses that are on disk
|
||||
//! ([`layer_prefetch`], `disk_cache::LayerPrefetch`), and the worker reads those entries
|
||||
//! into memory before rendering, so the lookups find them. Slow buffers are written by the
|
||||
//! worker after its frame and indexed here like frames (one least-recently-used order for
|
||||
//! both kinds).
|
||||
|
||||
use std::cell::RefCell;
|
||||
|
||||
use effectcraft_engine::render::disk_cache::{self, DiskIndex, Kind};
|
||||
use effectcraft_engine::render::disk_cache::{self, DiskIndex, Kind, LayerPrefetch};
|
||||
use serde_json::{Value, json};
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
@@ -29,7 +34,7 @@ extern "C" {
|
||||
#[wasm_bindgen(js_name = cacheList)]
|
||||
fn cache_list() -> js_sys::Promise;
|
||||
#[wasm_bindgen(js_name = cacheRead)]
|
||||
fn cache_read(name: &str) -> js_sys::Promise;
|
||||
pub(crate) fn cache_read(name: &str) -> js_sys::Promise;
|
||||
#[wasm_bindgen(js_name = cacheWrite)]
|
||||
pub(crate) fn cache_write(name: &str, bytes: js_sys::Uint8Array) -> js_sys::Promise;
|
||||
#[wasm_bindgen(js_name = cacheDelete)]
|
||||
@@ -53,6 +58,10 @@ struct State {
|
||||
writes: u64,
|
||||
evictions: u64,
|
||||
error: Option<String>,
|
||||
/// Recent layer-cache misses of the frame workers (what to prefetch).
|
||||
prefetch: LayerPrefetch,
|
||||
layer_hits: u64,
|
||||
layer_writes: u64,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
@@ -68,19 +77,19 @@ pub fn init() {
|
||||
wasm_bindgen_futures::spawn_local(async {
|
||||
match JsFuture::from(cache_list()).await {
|
||||
Ok(list) => {
|
||||
let mut entries: Vec<(f64, u128, u64)> = js_sys::Array::from(&list)
|
||||
let mut entries: Vec<(f64, Kind, u128, u64)> = js_sys::Array::from(&list)
|
||||
.iter()
|
||||
.filter_map(|e| {
|
||||
let get = |k: &str| js_sys::Reflect::get(&e, &k.into()).ok();
|
||||
let key = disk_cache::parse_entry_name(&get("name")?.as_string()?)?;
|
||||
Some((get("modified")?.as_f64().unwrap_or(0.0), key, get("size")?.as_f64().unwrap_or(0.0) as u64))
|
||||
let (kind, key) = parse_name(&get("name")?.as_string()?)?;
|
||||
Some((get("modified")?.as_f64().unwrap_or(0.0), kind, key, get("size")?.as_f64().unwrap_or(0.0) as u64))
|
||||
})
|
||||
.collect();
|
||||
entries.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
ST.with(|s| {
|
||||
let mut s = s.borrow_mut();
|
||||
for (_, key, bytes) in entries {
|
||||
s.index.insert(Kind::Frame, key, bytes);
|
||||
for (_, kind, key, bytes) in entries {
|
||||
s.index.insert(kind, key, bytes);
|
||||
}
|
||||
s.loaded = true;
|
||||
s.available = true;
|
||||
@@ -96,6 +105,46 @@ pub fn init() {
|
||||
});
|
||||
}
|
||||
|
||||
/// An entry's name in `js/host.js`'s cache folders (`layers/` for layer buffers).
|
||||
pub(crate) fn name_of(kind: Kind, key: u128) -> String {
|
||||
match kind {
|
||||
Kind::Frame => disk_cache::entry_name(key),
|
||||
Kind::Layer => format!("layers/{}", disk_cache::entry_name(key)),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_name(name: &str) -> Option<(Kind, u128)> {
|
||||
match name.strip_prefix("layers/") {
|
||||
Some(n) => Some((Kind::Layer, disk_cache::parse_entry_name(n)?)),
|
||||
None => Some((Kind::Frame, disk_cache::parse_entry_name(name)?)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Layer entries (32 hex digits) a frame worker should read before its next frame: recent
|
||||
/// misses that are on disk.
|
||||
pub fn layer_prefetch(max: usize) -> Vec<String> {
|
||||
if !active() {
|
||||
return vec![];
|
||||
}
|
||||
ST.with(|s| {
|
||||
let s = s.borrow();
|
||||
s.prefetch.plan(&s.index, max).into_iter().map(|k| format!("{k:032x}")).collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// A frame worker's layer-cache misses and the buffers it served from what it fetched.
|
||||
pub fn note_layers(misses: &[String], hits: &[String]) {
|
||||
let parse = |v: &[String]| v.iter().filter_map(|k| u128::from_str_radix(k, 16).ok()).collect::<Vec<u128>>();
|
||||
ST.with(|s| {
|
||||
let mut s = s.borrow_mut();
|
||||
s.prefetch.note_misses(parse(misses));
|
||||
for k in parse(hits) {
|
||||
s.index.touch(Kind::Layer, k);
|
||||
s.layer_hits += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Apply the settings: on/off and the limit (`max_gb`, at most half the origin's quota when it
|
||||
/// is known).
|
||||
pub fn configure(enabled: bool, max_gb: u32, quota: Option<u64>) {
|
||||
@@ -155,12 +204,15 @@ pub async fn read(key: u128) -> Option<disk_cache::Frame8> {
|
||||
frame
|
||||
}
|
||||
|
||||
/// A worker stored the frame of `key` (`bytes` on disk).
|
||||
pub fn stored(key: u128, bytes: u64) {
|
||||
/// A worker stored the frame (or, with `layer`, the layer buffer) of `key` (`bytes` on disk).
|
||||
pub fn stored(key: u128, bytes: u64, layer: bool) {
|
||||
ST.with(|s| {
|
||||
let mut s = s.borrow_mut();
|
||||
s.index.insert(Kind::Frame, key, bytes);
|
||||
s.index.insert(if layer { Kind::Layer } else { Kind::Frame }, key, bytes);
|
||||
s.writes += 1;
|
||||
if layer {
|
||||
s.layer_writes += 1;
|
||||
}
|
||||
});
|
||||
evict();
|
||||
}
|
||||
@@ -174,7 +226,7 @@ fn evict() {
|
||||
v
|
||||
});
|
||||
if !victims.is_empty() {
|
||||
delete(victims.into_iter().map(|(_, k)| disk_cache::entry_name(k)).collect());
|
||||
delete(victims.into_iter().map(|(kind, k)| name_of(kind, k)).collect());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,6 +246,7 @@ pub fn clear() -> (u64, u64) {
|
||||
let mut s = s.borrow_mut();
|
||||
let r = (s.index.len() as u64, s.index.total());
|
||||
s.index.clear();
|
||||
s.prefetch.clear();
|
||||
r
|
||||
});
|
||||
wasm_bindgen_futures::spawn_local(async {
|
||||
@@ -212,10 +265,12 @@ pub fn stats() -> Value {
|
||||
"enabled": s.enabled && s.available,
|
||||
"available": s.available,
|
||||
"loaded": s.loaded,
|
||||
"folder": "Origin Private File System: effectcraft-cache/v1/frames",
|
||||
"folder": "Origin Private File System: effectcraft-cache/v1 (frames, layers)",
|
||||
"entries": s.index.len(),
|
||||
"frames": s.index.len(),
|
||||
"layers": 0,
|
||||
"frames": s.index.keys(Kind::Frame).len(),
|
||||
"layers": s.index.keys(Kind::Layer).len(),
|
||||
"layerHits": s.layer_hits,
|
||||
"layerWrites": s.layer_writes,
|
||||
"bytes": s.index.total(),
|
||||
"maxBytes": s.max_bytes,
|
||||
"hits": s.hits,
|
||||
|
||||
Reference in New Issue
Block a user