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

This commit is contained in:
2026-08-09 05:12:19 +00:00
parent c6171dc625
commit 9a62922afa
11 changed files with 479 additions and 1 deletions

View 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
View File

@@ -395,6 +395,7 @@ version = "0.0.1"
dependencies = [ dependencies = [
"libremetaverse-openjpeg", "libremetaverse-openjpeg",
"libremetaverse-types", "libremetaverse-types",
"stats_alloc",
] ]
[[package]] [[package]]
@@ -427,6 +428,7 @@ version = "0.0.1"
dependencies = [ dependencies = [
"libremetaverse-imaging", "libremetaverse-imaging",
"libremetaverse-types", "libremetaverse-types",
"stats_alloc",
] ]
[[package]] [[package]]

View File

@@ -172,5 +172,9 @@ indexing deduplicates vertices per prim face, while bounded Wavefront OBJ
ingestion preserves object/group output and position/UV/normal associations. ingestion preserves object/group output and position/UV/normal associations.
Malformed dimensions, channel layouts, topology, and indices return typed Malformed dimensions, channel layouts, topology, and indices return typed
errors without partial meshes or panics. 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# The controlled audit aggregates every expected failure by standardized C#
member ID and rejects unrelated fixture, assertion, compile, or symbol errors. member ID and rejects unrelated fixture, assertion, compile, or symbol errors.

View File

@@ -15,5 +15,12 @@ jpeg2000 = ["dep:libremetaverse-openjpeg"]
libremetaverse-types = { path = "../libremetaverse-types" } libremetaverse-types = { path = "../libremetaverse-types" }
libremetaverse-openjpeg = { path = "../libremetaverse-openjpeg", optional = true } libremetaverse-openjpeg = { path = "../libremetaverse-openjpeg", optional = true }
[dev-dependencies]
stats_alloc = "0.1.10"
[[bench]]
name = "image_pipeline"
harness = false
[lints] [lints]
workspace = true workspace = true

View 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,
);
}

View File

@@ -11,5 +11,12 @@ description = "Primitive meshing shims for the MetaCrate LibreMetaverse rewrite"
libremetaverse-imaging = { path = "../libremetaverse-imaging" } libremetaverse-imaging = { path = "../libremetaverse-imaging" }
libremetaverse-types = { path = "../libremetaverse-types" } libremetaverse-types = { path = "../libremetaverse-types" }
[dev-dependencies]
stats_alloc = "0.1.10"
[[bench]]
name = "meshing"
harness = false
[lints] [lints]
workspace = true workspace = true

View 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,
);
}

View 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
View 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.

View File

@@ -146,5 +146,5 @@
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeUUID::test", "LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeUUID::test",
"LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeUndef::test" "LibreMetaverse.Tests/XmlLLSDTests.cs::XmlSDTests.DeserializeUndef::test"
], ],
"support_passes": 132 "support_passes": 134
} }

130
tools/check_milestone_06.py Normal file
View 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()