Complete imaging and meshing integration gate (#44)
Some checks failed
Imaging and meshing gate / native (macos-latest) (push) Has been cancelled
Imaging and meshing gate / native (ubuntu-latest) (push) Has been cancelled
Imaging and meshing gate / native (windows-latest) (push) Has been cancelled
JPEG 2000 feature / linux (push) Has been cancelled
JPEG 2000 feature / macos (push) Has been cancelled
JPEG 2000 feature / windows (push) Has been cancelled
Skia feature / linux (push) Has been cancelled
Skia feature / macos (push) Has been cancelled
Skia feature / windows (push) Has been cancelled
Some checks failed
Imaging and meshing gate / native (macos-latest) (push) Has been cancelled
Imaging and meshing gate / native (ubuntu-latest) (push) Has been cancelled
Imaging and meshing gate / native (windows-latest) (push) Has been cancelled
JPEG 2000 feature / linux (push) Has been cancelled
JPEG 2000 feature / macos (push) Has been cancelled
JPEG 2000 feature / windows (push) Has been cancelled
Skia feature / linux (push) Has been cancelled
Skia feature / macos (push) Has been cancelled
Skia feature / windows (push) Has been cancelled
This commit is contained in:
48
.gitea/workflows/imaging-meshing.yml
Normal file
48
.gitea/workflows/imaging-meshing.yml
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Imaging and meshing gate
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
native:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
RUSTDOCFLAGS: -D warnings
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy,rustfmt
|
||||
- name: Audit milestone ownership and feature isolation
|
||||
run: python tools/check_milestone_06.py
|
||||
- name: Test native imaging and meshing crates
|
||||
run: |
|
||||
cargo test -p libremetaverse-imaging
|
||||
cargo test -p libremetaverse-imaging-skia --no-default-features
|
||||
cargo test -p libremetaverse-prim-mesher
|
||||
- name: Test reviewed milestone parity cases
|
||||
run: |
|
||||
cargo test -p libremetaverse-compat-tests --test imaging_meshing_semantics reviewed_managedimagetests_
|
||||
cargo test -p libremetaverse-compat-tests --test imaging_meshing_semantics reviewed_coordtests_
|
||||
cargo test -p libremetaverse-compat-tests --test imaging_meshing_semantics reviewed_quattests_
|
||||
cargo test -p libremetaverse-compat-tests --test imaging_meshing_semantics reviewed_primmeshtests_
|
||||
- name: Compile workspace tests and performance baselines
|
||||
run: |
|
||||
cargo test --workspace --no-run
|
||||
cargo bench -p libremetaverse-imaging --bench image_pipeline --no-run
|
||||
cargo bench -p libremetaverse-prim-mesher --bench meshing --no-run
|
||||
- name: Lint and document milestone crates
|
||||
run: |
|
||||
cargo clippy -p libremetaverse-imaging -p libremetaverse-imaging-skia -p libremetaverse-prim-mesher --all-targets -- -D warnings
|
||||
cargo doc -p libremetaverse-imaging -p libremetaverse-imaging-skia -p libremetaverse-prim-mesher --no-deps
|
||||
- name: Verify formatting
|
||||
if: runner.os == 'Linux'
|
||||
run: cargo fmt --all -- --check
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -395,6 +395,7 @@ version = "0.0.1"
|
||||
dependencies = [
|
||||
"libremetaverse-openjpeg",
|
||||
"libremetaverse-types",
|
||||
"stats_alloc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -427,6 +428,7 @@ version = "0.0.1"
|
||||
dependencies = [
|
||||
"libremetaverse-imaging",
|
||||
"libremetaverse-types",
|
||||
"stats_alloc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -172,5 +172,9 @@ indexing deduplicates vertices per prim face, while bounded Wavefront OBJ
|
||||
ingestion preserves object/group output and position/UV/normal associations.
|
||||
Malformed dimensions, channel layouts, topology, and indices return typed
|
||||
errors without partial meshes or panics.
|
||||
The milestone-owned feature matrix, rendering-data contract, reproducible
|
||||
audit, cross-platform CI commands, bounds, and large-input performance
|
||||
baselines are documented in
|
||||
[`docs/imaging-meshing.md`](docs/imaging-meshing.md).
|
||||
The controlled audit aggregates every expected failure by standardized C#
|
||||
member ID and rejects unrelated fixture, assertion, compile, or symbol errors.
|
||||
|
||||
@@ -15,5 +15,12 @@ jpeg2000 = ["dep:libremetaverse-openjpeg"]
|
||||
libremetaverse-types = { path = "../libremetaverse-types" }
|
||||
libremetaverse-openjpeg = { path = "../libremetaverse-openjpeg", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
stats_alloc = "0.1.10"
|
||||
|
||||
[[bench]]
|
||||
name = "image_pipeline"
|
||||
harness = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
48
crates/libremetaverse-imaging/benches/image_pipeline.rs
Normal file
48
crates/libremetaverse-imaging/benches/image_pipeline.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use std::alloc::System;
|
||||
use std::hint::black_box;
|
||||
use std::time::Instant;
|
||||
|
||||
use libremetaverse_imaging::{ManagedImage, ManagedImageImageChannels};
|
||||
use stats_alloc::{INSTRUMENTED_SYSTEM, Region, StatsAlloc};
|
||||
|
||||
#[global_allocator]
|
||||
static ALLOCATOR: &StatsAlloc<System> = &INSTRUMENTED_SYSTEM;
|
||||
|
||||
fn main() {
|
||||
const INPUT_SIDE: i32 = 2_048;
|
||||
const OUTPUT_SIDE: i32 = 1_024;
|
||||
|
||||
let channels = ManagedImageImageChannels(
|
||||
ManagedImageImageChannels::COLOR.0 | ManagedImageImageChannels::ALPHA.0,
|
||||
);
|
||||
let mut source = ManagedImage::new(INPUT_SIDE, INPUT_SIDE, channels)
|
||||
.expect("large benchmark image must fit the documented pixel bound");
|
||||
for (index, red) in source.red.iter_mut().enumerate() {
|
||||
*red = u8::try_from(index & 0xff).expect("masked sample");
|
||||
}
|
||||
source.green.fill(97);
|
||||
source.blue.fill(193);
|
||||
source.alpha.fill(255);
|
||||
|
||||
// Warm the validation path before measuring the owned clone, resize, and export pipeline.
|
||||
source.validate().expect("valid benchmark image");
|
||||
let allocation_region = Region::new(ALLOCATOR);
|
||||
let started = Instant::now();
|
||||
let mut resized = source.clone().expect("bounded image clone");
|
||||
resized
|
||||
.resize_bilinear(OUTPUT_SIDE, OUTPUT_SIDE)
|
||||
.expect("bounded bilinear resize");
|
||||
let rgba = resized.export_raw().expect("bounded RGBA export");
|
||||
let elapsed = started.elapsed();
|
||||
let allocation_stats = allocation_region.change();
|
||||
|
||||
assert_eq!((resized.width, resized.height), (OUTPUT_SIDE, OUTPUT_SIDE));
|
||||
assert_eq!(rgba.len(), 4 * OUTPUT_SIDE as usize * OUTPUT_SIDE as usize);
|
||||
black_box((resized, rgba));
|
||||
println!(
|
||||
"image pipeline: {INPUT_SIDE}x{INPUT_SIDE} -> {OUTPUT_SIDE}x{OUTPUT_SIDE} in {elapsed:?}, {} allocations, {} reallocations, {} bytes allocated",
|
||||
allocation_stats.allocations,
|
||||
allocation_stats.reallocations,
|
||||
allocation_stats.bytes_allocated,
|
||||
);
|
||||
}
|
||||
@@ -11,5 +11,12 @@ description = "Primitive meshing shims for the MetaCrate LibreMetaverse rewrite"
|
||||
libremetaverse-imaging = { path = "../libremetaverse-imaging" }
|
||||
libremetaverse-types = { path = "../libremetaverse-types" }
|
||||
|
||||
[dev-dependencies]
|
||||
stats_alloc = "0.1.10"
|
||||
|
||||
[[bench]]
|
||||
name = "meshing"
|
||||
harness = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
54
crates/libremetaverse-prim-mesher/benches/meshing.rs
Normal file
54
crates/libremetaverse-prim-mesher/benches/meshing.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use std::alloc::System;
|
||||
use std::hint::black_box;
|
||||
use std::time::Instant;
|
||||
|
||||
use libremetaverse_prim_mesher::{PathType, PrimMesh};
|
||||
use stats_alloc::{INSTRUMENTED_SYSTEM, Region, StatsAlloc};
|
||||
|
||||
#[global_allocator]
|
||||
static ALLOCATOR: &StatsAlloc<System> = &INSTRUMENTED_SYSTEM;
|
||||
|
||||
fn main() {
|
||||
let mut mesh = PrimMesh::new(128, 0.03, 0.97, 0.35, 64)
|
||||
.expect("large benchmark profile must fit the documented bounds");
|
||||
mesh.viewer_mode = true;
|
||||
mesh.calc_vertex_normals = true;
|
||||
mesh.path_cut_begin = 0.02;
|
||||
mesh.path_cut_end = 0.98;
|
||||
mesh.hole_size_x = 0.7;
|
||||
mesh.hole_size_y = 0.3;
|
||||
mesh.twist_begin = -45;
|
||||
mesh.twist_end = 270;
|
||||
mesh.taper_x = 0.15;
|
||||
mesh.taper_y = -0.1;
|
||||
mesh.skew = 0.2;
|
||||
mesh.radius = 0.1;
|
||||
mesh.revolutions = 1.5;
|
||||
mesh.steps_per_revolution = 96;
|
||||
|
||||
let allocation_region = Region::new(ALLOCATOR);
|
||||
let started = Instant::now();
|
||||
mesh.extrude(PathType::Circular)
|
||||
.expect("bounded circular extrusion");
|
||||
let indexer = mesh
|
||||
.get_vertex_indexer()
|
||||
.expect("checked viewer indexing")
|
||||
.expect("viewer-mode indexer");
|
||||
let elapsed = started.elapsed();
|
||||
let allocation_stats = allocation_region.change();
|
||||
|
||||
assert!(!mesh.coords.is_empty());
|
||||
assert!(!mesh.faces.is_empty());
|
||||
assert!(!mesh.viewer_faces.is_empty());
|
||||
assert!(mesh.viewer_faces.len() <= mesh.faces.len());
|
||||
black_box((&mesh, &indexer));
|
||||
println!(
|
||||
"meshing pipeline: {} coordinates, {} triangles, {} prim faces in {elapsed:?}, {} allocations, {} reallocations, {} bytes allocated",
|
||||
mesh.coords.len(),
|
||||
mesh.faces.len(),
|
||||
indexer.num_prim_faces,
|
||||
allocation_stats.allocations,
|
||||
allocation_stats.reallocations,
|
||||
allocation_stats.bytes_allocated,
|
||||
);
|
||||
}
|
||||
111
crates/libremetaverse-prim-mesher/tests/rendering_contract.rs
Normal file
111
crates/libremetaverse-prim-mesher/tests/rendering_contract.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
//! Integration contracts for the data consumed by the future rendering pipeline.
|
||||
|
||||
use libremetaverse_imaging::{ManagedImage, ManagedImageImageChannels};
|
||||
use libremetaverse_prim_mesher::{
|
||||
PathType, PrimMesh, SculptMesh, SculptMeshSculptType, ViewerPolygon, ViewerVertex,
|
||||
};
|
||||
|
||||
fn assert_rendering_vertices(vertices: &[ViewerVertex], polygons: &[ViewerPolygon]) {
|
||||
assert!(vertices.len() <= usize::from(u16::MAX) + 1);
|
||||
for vertex in vertices {
|
||||
assert!(vertex.v.x.is_finite() && vertex.v.y.is_finite() && vertex.v.z.is_finite());
|
||||
assert!(vertex.n.x.is_finite() && vertex.n.y.is_finite() && vertex.n.z.is_finite());
|
||||
assert!(vertex.uv.u.is_finite() && vertex.uv.v.is_finite());
|
||||
}
|
||||
for polygon in polygons {
|
||||
for index in [polygon.v1, polygon.v2, polygon.v3] {
|
||||
let index = usize::try_from(index).expect("non-negative viewer index");
|
||||
assert!(index < vertices.len());
|
||||
u16::try_from(index).expect("MeshFoundry-facing index fits u16");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prim_mesh_viewer_groups_are_rendering_ready() {
|
||||
let mut mesh = PrimMesh::new(24, 0.05, 0.95, 0.25, 12).expect("checked profile");
|
||||
mesh.viewer_mode = true;
|
||||
mesh.calc_vertex_normals = true;
|
||||
mesh.path_cut_begin = 0.03;
|
||||
mesh.path_cut_end = 0.96;
|
||||
mesh.twist_begin = -30;
|
||||
mesh.twist_end = 120;
|
||||
mesh.taper_x = 0.1;
|
||||
mesh.skew = 0.15;
|
||||
mesh.revolutions = 1.25;
|
||||
mesh.steps_per_revolution = 48;
|
||||
mesh.extrude(PathType::Circular)
|
||||
.expect("checked viewer extrusion");
|
||||
|
||||
let indexer = mesh
|
||||
.get_vertex_indexer()
|
||||
.expect("checked viewer indexing")
|
||||
.expect("viewer mode produces an indexer");
|
||||
assert_eq!(indexer.num_prim_faces, mesh.num_prim_faces);
|
||||
assert_eq!(indexer.viewer_vertices.len(), indexer.viewer_polygons.len());
|
||||
for (vertices, polygons) in indexer
|
||||
.viewer_vertices
|
||||
.iter()
|
||||
.zip(indexer.viewer_polygons.iter())
|
||||
{
|
||||
assert_rendering_vertices(
|
||||
vertices,
|
||||
polygons
|
||||
.as_deref()
|
||||
.expect("allocated prim-face polygon group"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sculpt_lods_and_topologies_keep_rendering_indices_and_attributes_aligned() {
|
||||
let mut image = ManagedImage::new(128, 128, ManagedImageImageChannels::COLOR)
|
||||
.expect("bounded sculpt fixture");
|
||||
for index in 0..image.red.len() {
|
||||
image.red[index] = u8::try_from(index & 0xff).expect("masked sample");
|
||||
image.green[index] = u8::try_from((index / 128) & 0xff).expect("masked sample");
|
||||
image.blue[index] = u8::try_from((index * 17) & 0xff).expect("masked sample");
|
||||
}
|
||||
|
||||
for lod in [8, 16, 32] {
|
||||
for topology in [
|
||||
SculptMeshSculptType::Plane,
|
||||
SculptMeshSculptType::Sphere,
|
||||
SculptMeshSculptType::Torus,
|
||||
SculptMeshSculptType::Cylinder,
|
||||
] {
|
||||
let mesh =
|
||||
SculptMesh::new_with_managed_image_sculpt_type_int32_boolean_boolean_boolean(
|
||||
image.clone().expect("independent image fixture"),
|
||||
topology,
|
||||
lod,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
.expect("checked sculpt mesh");
|
||||
assert_eq!(mesh.coords.len(), mesh.normals.len());
|
||||
assert_eq!(mesh.coords.len(), mesh.uvs.len());
|
||||
assert_eq!(mesh.faces.len(), mesh.viewer_faces.len());
|
||||
assert!(mesh.coords.len() <= usize::from(u16::MAX) + 1);
|
||||
for face in &mesh.faces {
|
||||
for index in [face.v1, face.v2, face.v3] {
|
||||
let index = usize::try_from(index).expect("non-negative sculpt index");
|
||||
assert!(index < mesh.coords.len());
|
||||
u16::try_from(index).expect("MeshFoundry-facing sculpt index fits u16");
|
||||
}
|
||||
}
|
||||
for viewer in &mesh.viewer_faces {
|
||||
for coord in [viewer.v1, viewer.v2, viewer.v3] {
|
||||
assert!(coord.x.is_finite() && coord.y.is_finite() && coord.z.is_finite());
|
||||
}
|
||||
for normal in [viewer.n1, viewer.n2, viewer.n3] {
|
||||
assert!(normal.x.is_finite() && normal.y.is_finite() && normal.z.is_finite());
|
||||
}
|
||||
for uv in [viewer.uv1, viewer.uv2, viewer.uv3] {
|
||||
assert!(uv.u.is_finite() && uv.v.is_finite());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
67
docs/imaging-meshing.md
Normal file
67
docs/imaging-meshing.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Imaging and meshing integration
|
||||
|
||||
Milestone 06 owns the native implementations in `libremetaverse-imaging`,
|
||||
`libremetaverse-imaging-skia`, and `libremetaverse-prim-mesher`. The core
|
||||
imaging and meshing crates contain no CLR bridge, subprocess adapter, foreign
|
||||
RPC, or generated failure-only shim. The rendering crates consume the checked
|
||||
mesh data but remain a separate milestone: this boundary does not hide or
|
||||
reclassify their future `MeshFoundry` and `SimpleRenderer` work.
|
||||
|
||||
## Feature boundary
|
||||
|
||||
| Surface | Default build | Opt-in build | Native dependency |
|
||||
| --- | --- | --- | --- |
|
||||
| Managed images and TGA | enabled | n/a | none |
|
||||
| JPEG 2000 | disabled | `libremetaverse-imaging/jpeg2000` | OpenJPEG 2.5.x |
|
||||
| BMP/GIF/ICO/JPEG/PNG/WBMP/WebP | disabled | `libremetaverse-imaging-skia/skia` | Skia |
|
||||
| Prim and sculpt meshing, OBJ ingestion | enabled | n/a | none |
|
||||
|
||||
OpenJPEG is excluded from the default workspace and is reachable only through
|
||||
the optional dependency. Skia is an optional, target-specific dependency;
|
||||
Linux and Windows use the Vulkan-capable binary configuration while macOS uses
|
||||
the corresponding non-Vulkan binary configuration. Dedicated workflows build
|
||||
both optional features on Linux, macOS, and Windows. The default
|
||||
`imaging-meshing` workflow proves that no native codec is needed for the owned
|
||||
Rust and translated compatibility tests.
|
||||
|
||||
## Bounds and rendering contract
|
||||
|
||||
Decoded images are limited to 4,096 by 4,096 pixels and encoded inputs to 64
|
||||
MiB. Channel sizes, resize products, codec buffers, sculpt-map dimensions,
|
||||
profile/path sizes, mesh vertex counts, face indices, viewer groups, and OBJ
|
||||
indices use checked arithmetic before allocation or indexing. Malformed input
|
||||
returns a typed error.
|
||||
|
||||
The rendering-facing integration tests verify that PrimMesh and SculptMesh
|
||||
produce finite positions, normals, and UVs; aligned face and viewer-face data;
|
||||
preserved prim-face grouping; in-range indices; and the 16-bit index range used
|
||||
by the pinned MeshFoundry reference at every supported sculpt topology and LOD.
|
||||
|
||||
## Reproducible gates
|
||||
|
||||
Run the fixed ownership and feature audit with:
|
||||
|
||||
```console
|
||||
python3 tools/check_milestone_06.py
|
||||
```
|
||||
|
||||
The audit rejects owned Rust stubs, feature leakage, missing platform jobs,
|
||||
missing performance targets, or drift in the 48 reviewed ManagedImage,
|
||||
Coord/Quat, and PrimMesh compatibility cases. The cross-platform workflow also
|
||||
runs each reviewed group explicitly, native crate tests, workspace test
|
||||
compilation, Clippy with warnings denied, rustdoc with warnings denied, and
|
||||
benchmark compilation.
|
||||
|
||||
The focused performance baselines are executable Rust programs rather than
|
||||
wall-clock assertions, so slower shared runners do not weaken or destabilize
|
||||
correctness tests:
|
||||
|
||||
```console
|
||||
cargo bench -p libremetaverse-imaging --bench image_pipeline
|
||||
cargo bench -p libremetaverse-prim-mesher --bench meshing
|
||||
```
|
||||
|
||||
The image baseline clones, bilinearly resizes, and exports a 2,048-square RGBA
|
||||
image. The meshing baseline builds and indexes a high-detail cut, hollow,
|
||||
twisted circular extrusion. Both report elapsed time and allocation counts so
|
||||
regressions can be compared using the same inputs.
|
||||
@@ -146,5 +146,5 @@
|
||||
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeUUID::test",
|
||||
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeUndef::test"
|
||||
],
|
||||
"support_passes": 132
|
||||
"support_passes": 134
|
||||
}
|
||||
|
||||
130
tools/check_milestone_06.py
Normal file
130
tools/check_milestone_06.py
Normal file
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Audit the fixed ownership and feature boundary for milestone 06."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import tomllib
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OWNED_CRATES = (
|
||||
ROOT / "crates" / "libremetaverse-imaging",
|
||||
ROOT / "crates" / "libremetaverse-imaging-skia",
|
||||
ROOT / "crates" / "libremetaverse-prim-mesher",
|
||||
)
|
||||
OWNED_FIXTURES = {
|
||||
"LibreMetaverse.Tests/ManagedImageTests.cs": 3,
|
||||
"LibreMetaverse.Rendering.Tests/PrimMesher/CoordQuatTests.cs": 18,
|
||||
"LibreMetaverse.Rendering.Tests/PrimMesher/PrimMeshTests.cs": 27,
|
||||
}
|
||||
STUB_RE = re.compile(r"\b(?:not_implemented|unimplemented_api)\b|\b(?:todo|unimplemented)!\s*\(")
|
||||
|
||||
|
||||
def load_toml(path: Path) -> dict:
|
||||
with path.open("rb") as stream:
|
||||
return tomllib.load(stream)
|
||||
|
||||
|
||||
def check_owned_rust() -> None:
|
||||
offenders: list[str] = []
|
||||
for crate in OWNED_CRATES:
|
||||
for path in sorted((crate / "src").rglob("*.rs")):
|
||||
if STUB_RE.search(path.read_text()):
|
||||
offenders.append(path.relative_to(ROOT).as_posix())
|
||||
if offenders:
|
||||
raise SystemExit("milestone-owned Rust stubs remain: " + ", ".join(offenders))
|
||||
|
||||
|
||||
def check_features() -> None:
|
||||
imaging = load_toml(OWNED_CRATES[0] / "Cargo.toml")
|
||||
if imaging["features"].get("default") != []:
|
||||
raise SystemExit("imaging default features must stay empty")
|
||||
if imaging["features"].get("jpeg2000") != ["dep:libremetaverse-openjpeg"]:
|
||||
raise SystemExit("JPEG 2000 must stay isolated behind its optional feature")
|
||||
if not imaging["dependencies"]["libremetaverse-openjpeg"].get("optional"):
|
||||
raise SystemExit("the OpenJPEG adapter dependency must stay optional")
|
||||
|
||||
skia = load_toml(OWNED_CRATES[1] / "Cargo.toml")
|
||||
if skia["features"].get("default") != []:
|
||||
raise SystemExit("Skia adapter default features must stay empty")
|
||||
if skia["features"].get("skia") != ["dep:skia-safe"]:
|
||||
raise SystemExit("Skia must stay isolated behind its optional feature")
|
||||
targets = skia.get("target", {}).values()
|
||||
skia_dependencies = [
|
||||
target.get("dependencies", {}).get("skia-safe") for target in targets
|
||||
]
|
||||
if not skia_dependencies or any(
|
||||
dependency is None or not dependency.get("optional")
|
||||
for dependency in skia_dependencies
|
||||
):
|
||||
raise SystemExit("every platform-specific Skia dependency must stay optional")
|
||||
|
||||
workspace = load_toml(ROOT / "Cargo.toml")
|
||||
if "crates/libremetaverse-openjpeg" not in workspace["workspace"].get("exclude", []):
|
||||
raise SystemExit("the native OpenJPEG adapter must stay outside default workspace builds")
|
||||
|
||||
|
||||
def check_parity() -> None:
|
||||
catalog = json.loads((ROOT / "tests" / "upstream-tests.json").read_text())
|
||||
grouped: dict[str, list[dict]] = {source: [] for source in OWNED_FIXTURES}
|
||||
for case in catalog["tests"]:
|
||||
if case["source"] in grouped:
|
||||
grouped[case["source"]].append(case)
|
||||
for source, expected in OWNED_FIXTURES.items():
|
||||
cases = grouped[source]
|
||||
if len(cases) != expected:
|
||||
raise SystemExit(f"{source}: expected {expected} reviewed cases, found {len(cases)}")
|
||||
incomplete = [
|
||||
case["id"]
|
||||
for case in cases
|
||||
if case["status"] != "translated"
|
||||
or case.get("semantic_review") != "reviewed"
|
||||
or not (ROOT / case["rust_file"]).is_file()
|
||||
]
|
||||
if incomplete:
|
||||
raise SystemExit(f"{source}: incomplete parity cases: " + ", ".join(incomplete))
|
||||
|
||||
|
||||
def check_benchmarks_and_ci() -> None:
|
||||
expected_benches = {
|
||||
OWNED_CRATES[0]: "image_pipeline",
|
||||
OWNED_CRATES[2]: "meshing",
|
||||
}
|
||||
for crate, name in expected_benches.items():
|
||||
manifest = load_toml(crate / "Cargo.toml")
|
||||
benches = {entry["name"]: entry for entry in manifest.get("bench", [])}
|
||||
if benches.get(name, {}).get("harness") is not False:
|
||||
raise SystemExit(f"missing harness-free {name} benchmark declaration")
|
||||
if not (crate / "benches" / f"{name}.rs").is_file():
|
||||
raise SystemExit(f"missing {name} benchmark source")
|
||||
|
||||
workflows = {
|
||||
"imaging-meshing": ROOT / ".gitea" / "workflows" / "imaging-meshing.yml",
|
||||
"jpeg2000": ROOT / ".gitea" / "workflows" / "jpeg2000.yml",
|
||||
"skia": ROOT / ".gitea" / "workflows" / "skia.yml",
|
||||
}
|
||||
for name, path in workflows.items():
|
||||
if not path.is_file():
|
||||
raise SystemExit(f"missing {name} native-feature workflow")
|
||||
text = path.read_text().lower()
|
||||
missing = [platform for platform in ("linux", "macos", "windows") if platform not in text]
|
||||
if missing:
|
||||
raise SystemExit(f"{name} workflow lacks platforms: " + ", ".join(missing))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
check_owned_rust()
|
||||
check_features()
|
||||
check_parity()
|
||||
check_benchmarks_and_ci()
|
||||
print(
|
||||
"milestone 06 audit: owned Rust has no stubs; 48 reviewed parity cases, "
|
||||
"optional native features, benchmarks, and cross-platform workflows are complete"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user