The asset store now uses libs/sqlite_query as its ONLY engine — not a feature
flag, not a fallback. That closes the Windows gap (the embedded store starts
there now, and a SHARED->EXCLUSIVE upgrade is handled rather than assumed
free) and takes the C dependency out of the build everywhere else.
Around it:
- store: a garbage collector, catalogued content that is referenced in place
instead of copied, the `vjeffect` kind, and host/chat routes that keep up
with the chat wire below.
- importer: the unified map contract reaches quake2, quake3, doom and duke —
world placement, nav, welding, prelit maps and glTF node handling shared
rather than reimplemented per game. Music import, billboards and stateful
props move to the data crate so readers stop linking the importer.
- ai: the serving side of multi-lane chat — per-lane conversations, honest
progress and acceptance reporting, penalties and a watchdog, context as a
per-box number that compacts instead of erupting, a realtime session mode,
and inpaint/flux2 backends. `chat_bench` measures the rate the way the
client meter computes it.
- client / chat / chat_ui: a publication can NAME a file instead of carrying
it; the wire says whether a turn is warm and whether it is thinking, so a
client stops guessing; transcript and feed widgets render history the way
the model wrote it. `SessionConfig::catalog_runtime` lets a host size the
catalog runtime's lanes itself — a browsing UI puts every listing, every
per-tile resolve and every thumbnail blob through that one runtime and
wants a wider fast lane than the shared default, while media lanes keep
it (a few big transfers, not a thousand small ones).
- widgets: the shared asset widgets — one video view (knobbed seek,
transport, bracket trim, rail playback) used everywhere, plus thumb,
preview, scene view, walk-world and the lyric reader.
324 lines
13 KiB
Rust
324 lines
13 KiB
Rust
//! The thumbnail view-regions contract.
|
|
//!
|
|
//! A thumbnail is still ONE mandatory picture. `views` is metadata about that
|
|
//! picture — which parts of it are a spectrogram, a waveform strip, a packed
|
|
//! sprite sheet — so a consumer reads the layout instead of measuring pixels
|
|
//! and guessing. These tests pin the two properties that make that safe to
|
|
//! rely on: the kind numbers are frozen and append-only, and the five fields a
|
|
//! thumbnail has always carried stay exactly where they were on the wire.
|
|
|
|
mod common;
|
|
|
|
use common::*;
|
|
use makepad_asset_data::*;
|
|
|
|
/// Frozen wire numbers. A kind may be APPENDED with the next free tag; an
|
|
/// existing one may never be renumbered, because every stored manifest in
|
|
/// every catalog already means the old number.
|
|
#[test]
|
|
fn view_kind_tags_are_frozen_and_append_only() {
|
|
// The tags are not public API, so they are pinned through the only thing
|
|
// that is: the bytes. One view per kind, encoded on its own, and the tag
|
|
// is the first byte of the view block.
|
|
let tag_of = |kind: ThumbnailViewKind| -> u8 {
|
|
let mut thumb = thumbnail();
|
|
thumb.views = vec![ThumbnailView::whole(kind, thumb.width, thumb.height)];
|
|
let bytes = manifest_with(thumb).to_canonical_bytes().unwrap();
|
|
let at = views_offset(&bytes);
|
|
// count:u32 then the first view's kind tag.
|
|
bytes[at + 4]
|
|
};
|
|
assert_eq!(tag_of(ThumbnailViewKind::Image), 0);
|
|
assert_eq!(tag_of(ThumbnailViewKind::Fft), 1);
|
|
assert_eq!(tag_of(ThumbnailViewKind::Wave), 2);
|
|
assert_eq!(tag_of(ThumbnailViewKind::Anim), 3);
|
|
}
|
|
|
|
/// An unknown kind refuses rather than being taken for a known one: that is
|
|
/// what makes appending safe. A future `video` view read by today's decoder
|
|
/// is an error, never an `image`.
|
|
#[test]
|
|
fn an_unknown_kind_refuses() {
|
|
let mut thumb = thumbnail();
|
|
thumb.views = vec![ThumbnailView::whole(ThumbnailViewKind::Anim, 512, 512)];
|
|
let mut bytes = manifest_with(thumb).to_canonical_bytes().unwrap();
|
|
let at = views_offset(&bytes);
|
|
bytes[at + 4] = 99;
|
|
assert!(matches!(
|
|
AssetManifest::from_canonical_bytes(&bytes),
|
|
Err(AssetDataError::BadTag { what: "ThumbnailViewKind", found: 99 })
|
|
));
|
|
// And so does an unknown LAYOUT tag, for the same reason.
|
|
let mut bytes = manifest_with({
|
|
let mut t = thumbnail();
|
|
t.views = vec![ThumbnailView::whole(ThumbnailViewKind::Image, 512, 512)];
|
|
t
|
|
})
|
|
.to_canonical_bytes()
|
|
.unwrap();
|
|
let at = views_offset(&bytes);
|
|
bytes[at + 5] = 7;
|
|
assert!(matches!(
|
|
AssetManifest::from_canonical_bytes(&bytes),
|
|
Err(AssetDataError::BadTag { what: "ThumbnailLayout", found: 7 })
|
|
));
|
|
}
|
|
|
|
/// The compatibility promise, proved rather than asserted: a reader that
|
|
/// knows only the five fields a thumbnail carried before views existed walks
|
|
/// them off a NEW manifest and gets exactly the values the producer wrote.
|
|
/// The views ride behind them, where such a reader simply never looks.
|
|
#[test]
|
|
fn an_old_shaped_reader_still_parses_a_new_thumbnail() {
|
|
let mut thumb = thumbnail();
|
|
thumb.views = vec![
|
|
ThumbnailView::rect(ThumbnailViewKind::Fft, 0, 0, 512, 448),
|
|
ThumbnailView::rect(ThumbnailViewKind::Wave, 0, 448, 512, 64),
|
|
];
|
|
thumb.canonicalize();
|
|
let bytes = manifest_with(thumb.clone()).to_canonical_bytes().unwrap();
|
|
|
|
// Exactly the decode an old reader performed: blob, media, width, height,
|
|
// byte_len, in that order, at those widths, big-endian.
|
|
let at = find(&bytes, thumb.blob.as_bytes()).expect("thumbnail blob on the wire");
|
|
let mut p = at + 32;
|
|
let u8_ = |p: &mut usize| {
|
|
let v = bytes[*p];
|
|
*p += 1;
|
|
v
|
|
};
|
|
let u32_ = |p: &mut usize| {
|
|
let v = u32::from_be_bytes(bytes[*p..*p + 4].try_into().unwrap());
|
|
*p += 4;
|
|
v
|
|
};
|
|
let u64_ = |p: &mut usize| {
|
|
let v = u64::from_be_bytes(bytes[*p..*p + 8].try_into().unwrap());
|
|
*p += 8;
|
|
v
|
|
};
|
|
assert_eq!(u8_(&mut p), 0, "media tag: Png");
|
|
assert_eq!(u32_(&mut p), thumb.width);
|
|
assert_eq!(u32_(&mut p), thumb.height);
|
|
assert_eq!(u64_(&mut p), thumb.byte_len);
|
|
// Everything the old reader knew is consumed, and the picture it would
|
|
// fetch and draw is byte-identical to before. What it stopped at is the
|
|
// views block: a count it never knew to read.
|
|
assert_eq!(u32_(&mut p), 2, "and the new metadata sits behind it");
|
|
|
|
// The same five fields, from a thumbnail with NO views, land at the same
|
|
// offsets with the same values — views are additive, never a reshuffle.
|
|
let plain = ThumbnailMeta::plain(thumb.blob, thumb.media, thumb.width, thumb.height,
|
|
thumb.byte_len);
|
|
let plain_bytes = manifest_with(plain).to_canonical_bytes().unwrap();
|
|
let plain_at = find(&plain_bytes, thumb.blob.as_bytes()).unwrap();
|
|
assert_eq!(
|
|
&plain_bytes[plain_at..plain_at + 32 + 17],
|
|
&bytes[at..at + 32 + 17],
|
|
"the fixed head of a thumbnail is the same bytes either way"
|
|
);
|
|
}
|
|
|
|
/// Absence is not a claim: a thumbnail with no views round-trips, and says
|
|
/// nothing at all about being a sheet — which is exactly what every revision
|
|
/// baked before this field existed means.
|
|
#[test]
|
|
fn no_views_means_no_claim() {
|
|
let manifest = weapon_manifest();
|
|
let thumb = manifest.thumbnail.clone().unwrap();
|
|
assert!(thumb.views.is_empty());
|
|
assert_eq!(thumb.animation(), None);
|
|
assert_eq!(thumb.view(ThumbnailViewKind::Anim), None);
|
|
let back = AssetManifest::from_canonical_bytes(&manifest.to_canonical_bytes().unwrap()).unwrap();
|
|
assert_eq!(back, manifest);
|
|
}
|
|
|
|
/// Views round-trip through the canonical encoding, and a producer that found
|
|
/// them in any order ships identical bytes after `canonicalize` — or refuses,
|
|
/// rather than being silently reordered.
|
|
#[test]
|
|
fn views_round_trip_and_have_one_canonical_order() {
|
|
let composite = audio_composite();
|
|
let manifest = manifest_with(composite.clone());
|
|
let bytes = manifest.to_canonical_bytes().unwrap();
|
|
let back = AssetManifest::from_canonical_bytes(&bytes).unwrap();
|
|
assert_eq!(back.thumbnail.as_ref().unwrap(), &composite);
|
|
assert_eq!(back.to_canonical_bytes().unwrap(), bytes);
|
|
|
|
let mut shuffled = manifest_with({
|
|
let mut t = audio_composite();
|
|
t.views.reverse();
|
|
t
|
|
});
|
|
assert!(matches!(
|
|
shuffled.to_canonical_bytes(),
|
|
Err(AssetDataError::NotSorted { what: "thumbnail views" })
|
|
));
|
|
shuffled.canonicalize();
|
|
assert_eq!(shuffled.to_canonical_bytes().unwrap(), bytes);
|
|
|
|
// The same region declared twice is a producer bug, not a set.
|
|
let mut duped = manifest_with({
|
|
let mut t = audio_composite();
|
|
t.views.push(t.views[0]);
|
|
t.canonicalize();
|
|
t
|
|
});
|
|
duped.canonicalize();
|
|
assert!(matches!(
|
|
duped.to_canonical_bytes(),
|
|
Err(AssetDataError::Duplicate { what: "thumbnail views" })
|
|
));
|
|
}
|
|
|
|
/// A view must fit the picture it claims to describe. A region that runs off
|
|
/// the edge is a producer that stamped a layout it did not write, and a
|
|
/// consumer that trusted it would read someone else's pixels.
|
|
#[test]
|
|
fn a_view_must_fit_inside_its_picture() {
|
|
let refuse = |views: Vec<ThumbnailView>| {
|
|
let mut t = thumbnail();
|
|
t.views = views;
|
|
t.canonicalize();
|
|
manifest_with(t).to_canonical_bytes().unwrap_err()
|
|
};
|
|
// 512x512 picture.
|
|
assert!(matches!(
|
|
refuse(vec![ThumbnailView::rect(ThumbnailViewKind::Image, 0, 0, 513, 512)]),
|
|
AssetDataError::Malformed { what: "thumbnail view rect bounds" }
|
|
));
|
|
assert!(matches!(
|
|
refuse(vec![ThumbnailView::rect(ThumbnailViewKind::Wave, 0, 0, 0, 10)]),
|
|
AssetDataError::Malformed { what: "thumbnail view rect" }
|
|
));
|
|
// A near-overflow origin must refuse, not wrap into range.
|
|
assert!(matches!(
|
|
refuse(vec![ThumbnailView::rect(ThumbnailViewKind::Image, u32::MAX, 0, 8, 8)]),
|
|
AssetDataError::Malformed { what: "thumbnail view rect bounds" }
|
|
));
|
|
// Cells: 4 columns of 128 is exactly 512 wide; a fifth column is not.
|
|
let cells = |cols, count| ThumbnailCells { cols, cell_w: 128, cell_h: 128, first: 0, count };
|
|
assert!(matches!(
|
|
refuse(vec![ThumbnailView::cells(ThumbnailViewKind::Anim, cells(5, 5), 8.0)]),
|
|
AssetDataError::Malformed { what: "thumbnail view cells bounds" }
|
|
));
|
|
// 4x4 cells of 128 fill a 512x512 picture exactly, and are accepted.
|
|
let mut ok = thumbnail();
|
|
ok.views = vec![ThumbnailView::cells(ThumbnailViewKind::Anim, cells(4, 16), 8.0)];
|
|
manifest_with(ok).to_canonical_bytes().expect("a layout that fits is accepted");
|
|
// A seventeenth cell needs a fifth row.
|
|
assert!(matches!(
|
|
refuse(vec![ThumbnailView::cells(ThumbnailViewKind::Anim, cells(4, 17), 8.0)]),
|
|
AssetDataError::Malformed { what: "thumbnail view cells bounds" }
|
|
));
|
|
// An empty range describes nothing.
|
|
assert!(matches!(
|
|
refuse(vec![ThumbnailView::cells(ThumbnailViewKind::Anim, cells(4, 0), 8.0)]),
|
|
AssetDataError::Malformed { what: "thumbnail view cells" }
|
|
));
|
|
}
|
|
|
|
/// A cycling rate is bounded and positive: zero fps is a still declared as an
|
|
/// animation, and 10 000 fps is a producer bug that would spin a consumer's
|
|
/// frame clock.
|
|
#[test]
|
|
fn fps_is_bounded_and_positive() {
|
|
let with_fps = |fps: f32| {
|
|
let mut t = thumbnail();
|
|
t.views = vec![ThumbnailView {
|
|
kind: ThumbnailViewKind::Anim,
|
|
layout: ThumbnailLayout::Cells(ThumbnailCells {
|
|
cols: 4,
|
|
cell_w: 128,
|
|
cell_h: 128,
|
|
first: 0,
|
|
count: 4,
|
|
}),
|
|
fps: Some(fps),
|
|
}];
|
|
manifest_with(t).to_canonical_bytes()
|
|
};
|
|
assert!(with_fps(8.0).is_ok());
|
|
assert!(with_fps(240.0).is_ok());
|
|
assert!(matches!(
|
|
with_fps(0.0),
|
|
Err(AssetDataError::Malformed { what: "thumbnail view fps" })
|
|
));
|
|
assert!(matches!(
|
|
with_fps(-1.0),
|
|
Err(AssetDataError::Malformed { what: "thumbnail view fps" })
|
|
));
|
|
assert!(matches!(
|
|
with_fps(1000.0),
|
|
Err(AssetDataError::OverBudget { what: "thumbnail view fps", .. })
|
|
));
|
|
}
|
|
|
|
/// More regions than the budget refuses before allocation, on both sides.
|
|
#[test]
|
|
fn the_view_count_is_bounded() {
|
|
let mut t = thumbnail();
|
|
t.views = (0..9)
|
|
.map(|i| ThumbnailView::rect(ThumbnailViewKind::Image, i * 8, 0, 8, 8))
|
|
.collect();
|
|
assert!(matches!(
|
|
manifest_with(t).to_canonical_bytes(),
|
|
Err(AssetDataError::OverBudget { what: "thumbnail views", .. })
|
|
));
|
|
}
|
|
|
|
/// The two questions consumers actually ask, answered from the manifest:
|
|
/// "is this a sheet, and what is its layout" and "where is the wave strip".
|
|
#[test]
|
|
fn the_accessors_answer_the_questions_consumers_used_to_guess() {
|
|
let composite = audio_composite();
|
|
assert_eq!(composite.animation(), None, "a spectrogram is not a sprite sheet");
|
|
assert!(matches!(
|
|
composite.view(ThumbnailViewKind::Wave).map(|v| v.layout),
|
|
Some(ThumbnailLayout::Rect(ThumbnailRect { x: 0, y: 448, w: 512, h: 64 }))
|
|
));
|
|
|
|
let mut sheet = thumbnail();
|
|
let cells = ThumbnailCells { cols: 4, cell_w: 128, cell_h: 128, first: 0, count: 11 };
|
|
sheet.views = vec![ThumbnailView::cells(ThumbnailViewKind::Anim, cells, 12.0)];
|
|
let (found, fps) = sheet.animation().expect("a declared sheet says so");
|
|
assert_eq!(found, cells);
|
|
assert_eq!(fps, 12.0);
|
|
// Eleven real frames in a 4x4 grid: the five padding cells the packer
|
|
// wrote to clear the 256px floor are NOT frames, and nobody has to scan
|
|
// pixels to find that out.
|
|
assert_eq!(found.count, 11);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// The audio composite the importer bakes: FFT over most of the picture, a
|
|
/// thin wave strip along the bottom edge, both declared.
|
|
fn audio_composite() -> ThumbnailMeta {
|
|
let mut t = thumbnail();
|
|
t.views = vec![
|
|
ThumbnailView::rect(ThumbnailViewKind::Fft, 0, 0, 512, 448),
|
|
ThumbnailView::rect(ThumbnailViewKind::Wave, 0, 448, 512, 64),
|
|
];
|
|
t.canonicalize();
|
|
t
|
|
}
|
|
|
|
fn manifest_with(thumbnail: ThumbnailMeta) -> AssetManifest {
|
|
let mut m = weapon_manifest();
|
|
m.thumbnail = Some(thumbnail);
|
|
m
|
|
}
|
|
|
|
/// Byte offset of the views count inside an encoded manifest: the thumbnail
|
|
/// blob, then the 17 fixed bytes an old reader consumes.
|
|
fn views_offset(bytes: &[u8]) -> usize {
|
|
let thumb = weapon_manifest().thumbnail.clone().unwrap();
|
|
find(bytes, thumb.blob.as_bytes()).expect("thumbnail blob on the wire") + 32 + 17
|
|
}
|
|
|
|
fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
|
|
haystack.windows(needle.len()).position(|w| w == needle)
|
|
}
|