From 1c2848c780e31bb9b0c4d11af982eca755717e53 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Tue, 11 Aug 2026 04:09:22 +0000 Subject: [PATCH] Complete native extension milestone gate (#84) --- .gitea/workflows/rust-workspace.yml | 23 +- api/SHIM-COVERAGE.md | 2 +- crates/libremetaverse-rlv/src/generated.rs | 210 ++------------- docs/extension-milestone-gate.md | 113 ++++++++ tests/compat/tests/extension_shims.rs | 5 +- .../compat/tests/milestone_10_integration.rs | 80 ++++++ tools/check_milestone_10.py | 249 ++++++++++++++++++ tools/check_milestone_10_issue_81.py | 2 +- tools/check_milestone_10_issue_82.py | 2 +- tools/generate_api_shims.py | 37 ++- tools/test_milestone_10.py | 77 ++++++ 11 files changed, 604 insertions(+), 196 deletions(-) create mode 100644 docs/extension-milestone-gate.md create mode 100644 tests/compat/tests/milestone_10_integration.rs create mode 100644 tools/check_milestone_10.py create mode 100644 tools/test_milestone_10.py diff --git a/.gitea/workflows/rust-workspace.yml b/.gitea/workflows/rust-workspace.yml index eb86254..f143d6c 100644 --- a/.gitea/workflows/rust-workspace.yml +++ b/.gitea/workflows/rust-workspace.yml @@ -15,8 +15,12 @@ on: - "tools/check_milestone_10_issue_81.py" - "tools/check_milestone_10_issue_82.py" - "tools/check_milestone_10_issue_83.py" + - "tools/check_milestone_10.py" + - "tools/test_milestone_10.py" - "tools/generate_lsl_tables.py" - "codegen/inputs/lsl_tools_grammar.json" + - "docs/extension-milestone-gate.md" + - "api/SHIM-COVERAGE.md" - "**/*.rs" - "**/Cargo.toml" - "Cargo.lock" @@ -34,8 +38,12 @@ on: - "tools/check_milestone_10_issue_81.py" - "tools/check_milestone_10_issue_82.py" - "tools/check_milestone_10_issue_83.py" + - "tools/check_milestone_10.py" + - "tools/test_milestone_10.py" - "tools/generate_lsl_tables.py" - "codegen/inputs/lsl_tools_grammar.json" + - "docs/extension-milestone-gate.md" + - "api/SHIM-COVERAGE.md" - "**/*.rs" - "**/Cargo.toml" - "Cargo.lock" @@ -48,11 +56,15 @@ jobs: OPENJPEG_PREFIX: /tmp/metacrate-openjpeg-${{ github.run_id }} PKG_CONFIG_PATH: /tmp/metacrate-openjpeg-${{ github.run_id }}/lib/pkgconfig LD_LIBRARY_PATH: /tmp/metacrate-openjpeg-${{ github.run_id }}/lib + CARGO_BUILD_JOBS: 1 + CARGO_INCREMENTAL: 0 + CARGO_PROFILE_DEV_DEBUG: 0 + CARGO_PROFILE_TEST_DEBUG: 0 steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: - components: rustfmt + components: rustfmt, clippy - name: Install audited OpenJPEG 2.5.4 run: | sudo apt-get update @@ -80,7 +92,16 @@ jobs: python3 tools/check_milestone_10_issue_82.py python3 tools/check_milestone_10_issue_83.py python3 tools/generate_lsl_tables.py --check + python3 tools/check_milestone_10.py - name: Test the complete native world milestone run: python3 tools/test_milestone_09.py + - name: Test the complete native extension milestone + run: python3 tools/test_milestone_10.py - name: Compile every workspace target with bounded memory run: cargo check --workspace --all-targets --locked -j 1 + - name: Lint every native extension target with bounded memory + run: cargo clippy -p libremetaverse-rendering-simple -p libremetaverse-rendering-mesh-foundry -p libremetaverse-rlv -p libremetaverse-lsl-tools --all-targets --locked -j 1 -- -D warnings + - name: Document every native extension crate + env: + RUSTDOCFLAGS: -D warnings + run: cargo doc -p libremetaverse-rendering-simple -p libremetaverse-rendering-mesh-foundry -p libremetaverse-rlv -p libremetaverse-lsl-tools --no-deps --locked -j 1 diff --git a/api/SHIM-COVERAGE.md b/api/SHIM-COVERAGE.md index c73773a..5750e25 100644 --- a/api/SHIM-COVERAGE.md +++ b/api/SHIM-COVERAGE.md @@ -9,7 +9,7 @@ Generated by `python3 tools/generate_api_shims.py`; do not edit by hand. | `LibreMetaverse.Imaging.Skia` | 1 | 3 | native implementation: 1 type / 3 members; no generated shims remain | | `LibreMetaverse.LslTools` | 164 | 768 | native implementation: 164 types / 768 members; no generated shims remain | | `LibreMetaverse.PrimMesher` | 17 | 207 | native implementation: 15 types / 200 members; remaining surface is callable failure-only shims | -| `LibreMetaverse.RLV` | 28 | 499 | native implementation: 17 types / 200 members; remaining surface is callable failure-only shims | +| `LibreMetaverse.RLV` | 28 | 499 | native implementation: 28 types / 499 members; no generated shims remain | | `LibreMetaverse.Rendering.MeshFoundry` | 1 | 14 | native implementation: 1 type / 14 members; no generated shims remain | | `LibreMetaverse.Rendering.Simple` | 1 | 6 | native implementation: 1 type / 6 members; no generated shims remain | | `LibreMetaverse.StructuredData` | 16 | 295 | native implementation: 16 types / 295 members; no generated shims remain | diff --git a/crates/libremetaverse-rlv/src/generated.rs b/crates/libremetaverse-rlv/src/generated.rs index d50c595..4206f0d 100644 --- a/crates/libremetaverse-rlv/src/generated.rs +++ b/crates/libremetaverse-rlv/src/generated.rs @@ -131,188 +131,90 @@ pub trait IRlvActionCallbacks: Send + Sync { factor: f32, delta: f32, cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> std::pin::Pin> + Send + '_>> - { - Box::pin(async { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.IRlvActionCallbacks.AdjustHeightAsync(System.Single,System.Single,System.Single,System.Threading.CancellationToken)", - ) - }) - } + ) -> std::pin::Pin> + Send + '_>>; /// C# member: `M:LibreMetaverse.RLV.IRlvActionCallbacks.AttachAsync(System.Collections.Generic.IReadOnlyList{LibreMetaverse.RLV.AttachmentRequest},System.Threading.CancellationToken)`. fn attach( &self, items_to_attach: Vec, cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> std::pin::Pin> + Send + '_>> - { - Box::pin(async { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.IRlvActionCallbacks.AttachAsync(System.Collections.Generic.IReadOnlyList{LibreMetaverse.RLV.AttachmentRequest},System.Threading.CancellationToken)", - ) - }) - } + ) -> std::pin::Pin> + Send + '_>>; /// C# member: `M:LibreMetaverse.RLV.IRlvActionCallbacks.DetachAsync(System.Collections.Generic.IReadOnlyList{System.Guid},System.Threading.CancellationToken)`. fn detach( &self, item_ids: Vec, cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> std::pin::Pin> + Send + '_>> - { - Box::pin(async { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.IRlvActionCallbacks.DetachAsync(System.Collections.Generic.IReadOnlyList{System.Guid},System.Threading.CancellationToken)", - ) - }) - } + ) -> std::pin::Pin> + Send + '_>>; /// C# member: `M:LibreMetaverse.RLV.IRlvActionCallbacks.RemOutfitAsync(System.Collections.Generic.IReadOnlyList{System.Guid},System.Threading.CancellationToken)`. fn rem_outfit( &self, item_ids: Vec, cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> std::pin::Pin> + Send + '_>> - { - Box::pin(async { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.IRlvActionCallbacks.RemOutfitAsync(System.Collections.Generic.IReadOnlyList{System.Guid},System.Threading.CancellationToken)", - ) - }) - } + ) -> std::pin::Pin> + Send + '_>>; /// C# member: `M:LibreMetaverse.RLV.IRlvActionCallbacks.SendInstantMessageAsync(System.Guid,System.String,System.Threading.CancellationToken)`. fn send_instant_message( &self, target_user: libremetaverse_types::compat::Guid, message: String, cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> std::pin::Pin> + Send + '_>> - { - Box::pin(async { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.IRlvActionCallbacks.SendInstantMessageAsync(System.Guid,System.String,System.Threading.CancellationToken)", - ) - }) - } + ) -> std::pin::Pin> + Send + '_>>; /// C# member: `M:LibreMetaverse.RLV.IRlvActionCallbacks.SendReplyAsync(System.Int32,System.String,System.Threading.CancellationToken)`. fn send_reply( &self, channel: i32, message: String, cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> std::pin::Pin> + Send + '_>> - { - Box::pin(async { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.IRlvActionCallbacks.SendReplyAsync(System.Int32,System.String,System.Threading.CancellationToken)", - ) - }) - } + ) -> std::pin::Pin> + Send + '_>>; /// C# member: `M:LibreMetaverse.RLV.IRlvActionCallbacks.SetCamFOVAsync(System.Single,System.Threading.CancellationToken)`. fn set_cam_fov( &self, fov_in_radians: f32, cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> std::pin::Pin> + Send + '_>> - { - Box::pin(async { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.IRlvActionCallbacks.SetCamFOVAsync(System.Single,System.Threading.CancellationToken)", - ) - }) - } + ) -> std::pin::Pin> + Send + '_>>; /// C# member: `M:LibreMetaverse.RLV.IRlvActionCallbacks.SetDebugAsync(System.String,System.String,System.Threading.CancellationToken)`. fn set_debug( &self, setting_name: String, setting_value: String, cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> std::pin::Pin> + Send + '_>> - { - Box::pin(async { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.IRlvActionCallbacks.SetDebugAsync(System.String,System.String,System.Threading.CancellationToken)", - ) - }) - } + ) -> std::pin::Pin> + Send + '_>>; /// C# member: `M:LibreMetaverse.RLV.IRlvActionCallbacks.SetEnvAsync(System.String,System.String,System.Threading.CancellationToken)`. fn set_env( &self, setting_name: String, setting_value: String, cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> std::pin::Pin> + Send + '_>> - { - Box::pin(async { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.IRlvActionCallbacks.SetEnvAsync(System.String,System.String,System.Threading.CancellationToken)", - ) - }) - } + ) -> std::pin::Pin> + Send + '_>>; /// C# member: `M:LibreMetaverse.RLV.IRlvActionCallbacks.SetGroupAsync(System.Guid,System.String,System.Threading.CancellationToken)`. fn set_group_with_guid_string_cancellation_token( &self, group_id: libremetaverse_types::compat::Guid, role_name: Option, cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> std::pin::Pin> + Send + '_>> - { - Box::pin(async { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.IRlvActionCallbacks.SetGroupAsync(System.Guid,System.String,System.Threading.CancellationToken)", - ) - }) - } + ) -> std::pin::Pin> + Send + '_>>; /// C# member: `M:LibreMetaverse.RLV.IRlvActionCallbacks.SetGroupAsync(System.String,System.String,System.Threading.CancellationToken)`. fn set_group_with_string_string_cancellation_token( &self, group_name: String, role_name: Option, cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> std::pin::Pin> + Send + '_>> - { - Box::pin(async { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.IRlvActionCallbacks.SetGroupAsync(System.String,System.String,System.Threading.CancellationToken)", - ) - }) - } + ) -> std::pin::Pin> + Send + '_>>; /// C# member: `M:LibreMetaverse.RLV.IRlvActionCallbacks.SetRotAsync(System.Single,System.Threading.CancellationToken)`. fn set_rot( &self, angle_in_radians: f32, cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> std::pin::Pin> + Send + '_>> - { - Box::pin(async { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.IRlvActionCallbacks.SetRotAsync(System.Single,System.Threading.CancellationToken)", - ) - }) - } + ) -> std::pin::Pin> + Send + '_>>; /// C# member: `M:LibreMetaverse.RLV.IRlvActionCallbacks.SitAsync(System.Guid,System.Threading.CancellationToken)`. fn sit( &self, target: libremetaverse_types::compat::Guid, cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> std::pin::Pin> + Send + '_>> - { - Box::pin(async { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.IRlvActionCallbacks.SitAsync(System.Guid,System.Threading.CancellationToken)", - ) - }) - } + ) -> std::pin::Pin> + Send + '_>>; /// C# member: `M:LibreMetaverse.RLV.IRlvActionCallbacks.SitGroundAsync(System.Threading.CancellationToken)`. fn sit_ground( &self, cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> std::pin::Pin> + Send + '_>> - { - Box::pin(async { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.IRlvActionCallbacks.SitGroundAsync(System.Threading.CancellationToken)", - ) - }) - } + ) -> std::pin::Pin> + Send + '_>>; /// C# member: `M:LibreMetaverse.RLV.IRlvActionCallbacks.TpToAsync(System.Single,System.Single,System.Single,System.String,System.Nullable{System.Single},System.Threading.CancellationToken)`. fn tp_to( &self, @@ -322,26 +224,12 @@ pub trait IRlvActionCallbacks: Send + Sync { region_name: Option, lookat: Option>, cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> std::pin::Pin> + Send + '_>> - { - Box::pin(async { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.IRlvActionCallbacks.TpToAsync(System.Single,System.Single,System.Single,System.String,System.Nullable{System.Single},System.Threading.CancellationToken)", - ) - }) - } + ) -> std::pin::Pin> + Send + '_>>; /// C# member: `M:LibreMetaverse.RLV.IRlvActionCallbacks.UnsitAsync(System.Threading.CancellationToken)`. fn unsit( &self, cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> std::pin::Pin> + Send + '_>> - { - Box::pin(async { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.IRlvActionCallbacks.UnsitAsync(System.Threading.CancellationToken)", - ) - }) - } + ) -> std::pin::Pin> + Send + '_>>; } /// C# type: `T:LibreMetaverse.RLV.IRlvQueryCallbacks`. @@ -350,40 +238,20 @@ pub trait IRlvQueryCallbacks: Send + Sync { fn is_sitting( &self, cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> std::pin::Pin> + Send + '_>> - { - Box::pin(async { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.IRlvQueryCallbacks.IsSittingAsync(System.Threading.CancellationToken)", - ) - }) - } + ) -> std::pin::Pin> + Send + '_>>; /// C# member: `M:LibreMetaverse.RLV.IRlvQueryCallbacks.ObjectExistsAsync(System.Guid,System.Threading.CancellationToken)`. fn object_exists( &self, object_id: libremetaverse_types::compat::Guid, cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> std::pin::Pin> + Send + '_>> - { - Box::pin(async { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.IRlvQueryCallbacks.ObjectExistsAsync(System.Guid,System.Threading.CancellationToken)", - ) - }) - } + ) -> std::pin::Pin> + Send + '_>>; /// C# member: `M:LibreMetaverse.RLV.IRlvQueryCallbacks.TryGetActiveGroupNameAsync(System.Threading.CancellationToken)`. fn try_get_active_group_name( &self, cancellation_token: libremetaverse_types::compat::CancellationToken, ) -> std::pin::Pin< Box> + Send + '_>, - > { - Box::pin(async { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.IRlvQueryCallbacks.TryGetActiveGroupNameAsync(System.Threading.CancellationToken)", - ) - }) - } + >; /// C# member: `M:LibreMetaverse.RLV.IRlvQueryCallbacks.TryGetCameraSettingsAsync(System.Threading.CancellationToken)`. fn try_get_camera_settings( &self, @@ -398,13 +266,7 @@ pub trait IRlvQueryCallbacks: Send + Sync { > + Send + '_, >, - > { - Box::pin(async { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.IRlvQueryCallbacks.TryGetCameraSettingsAsync(System.Threading.CancellationToken)", - ) - }) - } + >; /// C# member: `M:LibreMetaverse.RLV.IRlvQueryCallbacks.TryGetDebugSettingValueAsync(System.String,System.Threading.CancellationToken)`. fn try_get_debug_setting_value( &self, @@ -412,13 +274,7 @@ pub trait IRlvQueryCallbacks: Send + Sync { cancellation_token: libremetaverse_types::compat::CancellationToken, ) -> std::pin::Pin< Box> + Send + '_>, - > { - Box::pin(async { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.IRlvQueryCallbacks.TryGetDebugSettingValueAsync(System.String,System.Threading.CancellationToken)", - ) - }) - } + >; /// C# member: `M:LibreMetaverse.RLV.IRlvQueryCallbacks.TryGetEnvironmentSettingValueAsync(System.String,System.Threading.CancellationToken)`. fn try_get_environment_setting_value( &self, @@ -426,13 +282,7 @@ pub trait IRlvQueryCallbacks: Send + Sync { cancellation_token: libremetaverse_types::compat::CancellationToken, ) -> std::pin::Pin< Box> + Send + '_>, - > { - Box::pin(async { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.IRlvQueryCallbacks.TryGetEnvironmentSettingValueAsync(System.String,System.Threading.CancellationToken)", - ) - }) - } + >; /// C# member: `M:LibreMetaverse.RLV.IRlvQueryCallbacks.TryGetInventoryMapAsync(System.Threading.CancellationToken)`. fn try_get_inventory_map( &self, @@ -444,13 +294,7 @@ pub trait IRlvQueryCallbacks: Send + Sync { > + Send + '_, >, - > { - Box::pin(async { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.IRlvQueryCallbacks.TryGetInventoryMapAsync(System.Threading.CancellationToken)", - ) - }) - } + >; /// C# member: `M:LibreMetaverse.RLV.IRlvQueryCallbacks.TryGetSitIdAsync(System.Threading.CancellationToken)`. fn try_get_sit_id( &self, @@ -462,13 +306,7 @@ pub trait IRlvQueryCallbacks: Send + Sync { > + Send + '_, >, - > { - Box::pin(async { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.RLV.IRlvQueryCallbacks.TryGetSitIdAsync(System.Threading.CancellationToken)", - ) - }) - } + >; } /// C# type: `T:LibreMetaverse.RLV.InventoryMap`. diff --git a/docs/extension-milestone-gate.md b/docs/extension-milestone-gate.md new file mode 100644 index 0000000..dfcd795 --- /dev/null +++ b/docs/extension-milestone-gate.md @@ -0,0 +1,113 @@ +# Rendering, RLV, and LSL extension gate + +Milestone 10 replaces the pinned rendering, Restrained Love Viewer, and LSL +tooling assemblies with native Rust. The four extension crates are normal +workspace dependencies with no opt-in compatibility feature: adding one of +them selects its Rust implementation on every supported platform. None loads +a CLR assembly, starts a .NET process, or delegates to a platform-only API. + +## Choosing an extension + +| Previous C# assembly | Rust crate | Primary entry points | +| --- | --- | --- | +| `LibreMetaverse.Rendering.Simple` | `libremetaverse-rendering-simple` | `SimpleRenderer`, `IRendering` | +| `LibreMetaverse.Rendering.MeshFoundry` | `libremetaverse-rendering-mesh-foundry` | `MeshFoundry`, `IRendering`, `MeshFaceAux` | +| `LibreMetaverse.RLV` | `libremetaverse-rlv` | `RlvService`, `RlvPermissionsService`, callback traits | +| `LibreMetaverse.LslTools` | `libremetaverse-lsl-tools` | `Lexer`, `Parser`, `Grammar`, generated tables | + +The mapped PascalCase C# surface remains represented in the API ledger, while +the callable Rust names follow the workspace's snake-case mapping. New code +should use owned Rust values and `Result` instead of nullable CLR containers or +exceptions. `api/SHIM-COVERAGE.md` is generated evidence that all 194 types and +1,287 members in these four assemblies are native; the milestone audit rejects +an owned `not_implemented`, `unimplemented_api`, `todo!`, or `unimplemented!`. + +## Rendering migration + +Both renderers consume the native core `Primitive` and common `IRendering` +types. Use `SimpleRenderer` for prim and decoded sculpt geometry. Use +`MeshFoundry` when an LLSD mesh asset, skin weights, secondary UVs, tangents, +terrain, or physics hulls are required. Both keep coordinates prim-local; the +world service remains responsible for object transforms, asset retrieval, and +scene ownership. + +```rust,no_run +use libremetaverse::Primitive; +use libremetaverse::rendering::{DetailLevel, IRendering}; +use libremetaverse_rendering_simple::SimpleRenderer; + +fn render(primitive: Primitive) -> Result { + let renderer = SimpleRenderer::new()?; + Ok(renderer + .generate_simple_mesh(primitive, DetailLevel::High)? + .indices + .len()) +} +``` + +Inputs are bounded before allocation: a face stays in the 16-bit index domain, +and MeshFoundry caps encoded/decompressed assets, sections, total geometry, +joints, and convex data. Rendering is deterministic and performs no network, +GPU, window-system, or callback work. + +## RLV host integration and teardown + +`RlvService` separates deterministic protocol/state handling from world side +effects. A host implements every required method of `IRlvQueryCallbacks` and +`IRlvActionCallbacks`, then passes owned trait objects to `RlvService::new`. +There are no generated fallback methods: an incomplete adapter fails to +compile. `RlvCallbacksDefault` and `RlvActionCallbacksDefault` are complete, +cancellation-aware adapters for hosts that intentionally use conservative +query values and no-op actions. + +```rust,no_run +use libremetaverse_rlv::{RlvActionCallbacksDefault, RlvCallbacksDefault, RlvService}; + +fn service() -> Result { + RlvService::new( + Box::new(RlvCallbacksDefault::new()?), + Box::new(RlvActionCallbacksDefault::new()?), + true, + ) +} +``` + +Core/world adapters should snapshot manager data before returning a future and +perform network actions through the corresponding manager APIs. The RLV crate +releases its locks before awaiting a callback, checks cancellation between +commands, bounds each message to 64 KiB and 128 commands, and never starts a +background task. Drop event `Subscription` guards when a host shuts down; drop +the service and its callback objects after outstanding host futures complete. + +## LSL tooling migration + +Use `generated_lexer()` and `generated_parser()` for the reviewed checked-in +grammar, or build typed `Dfa` and `Grammar` values for another language. Source +locations use UTF-16 code units to preserve the mapped contract. Generation is +an explicit development operation; parsing never generates or compiles code at +runtime. + +```rust,no_run +use libremetaverse_lsl_tools::{Lexer, Parser, generated_lexer, generated_parser}; + +fn parse(source: String) -> Result { + let lexer = Lexer::new(generated_lexer()?)?; + let mut parser = Parser::new(generated_parser()?, lexer)?; + Ok(parser.parse_with_string(source)?.yyname()) +} +``` + +Source, token, DFA, parser-state, stack, operation, recovery, and generator +sizes have fixed limits. The checked table is reproducible with +`python3 tools/generate_lsl_tables.py --check`; API generation is checked with +`python3 tools/generate_api_shims.py --check`. + +## Complete milestone verification + +`python3 tools/check_milestone_10.py` audits native ownership, reviewed parity +metadata, deterministic generation, resource limits, integration evidence, +migration documentation, and the ubuntu-only workflow. Run +`python3 tools/test_milestone_10.py` to execute the four crate suites, all 616 +reviewed RLV compatibility cases, rendering compatibility, the cross-extension +core/world integration, and the compile-only mapped API fixture. The workflow +runs these gates on `ubuntu-latest`. diff --git a/tests/compat/tests/extension_shims.rs b/tests/compat/tests/extension_shims.rs index 060c5cc..efd088f 100644 --- a/tests/compat/tests/extension_shims.rs +++ b/tests/compat/tests/extension_shims.rs @@ -67,10 +67,7 @@ fn extension_flows_have_typed_callable_signatures() { #[test] fn extension_constructors_have_expected_native_or_failure_behavior() { assert!(YyParser::new().is_ok()); - assert_eq!( - member_id(RlvActionCallbacksDefault::new()), - "M:LibreMetaverse.RLV.RlvActionCallbacksDefault.#ctor" - ); + assert!(RlvActionCallbacksDefault::new().is_ok()); assert_eq!( member_id(TCPPipe::new()), "M:LibreMetaverse.Voice.Vivox.TCPPipe.#ctor" diff --git a/tests/compat/tests/milestone_10_integration.rs b/tests/compat/tests/milestone_10_integration.rs new file mode 100644 index 0000000..c30e574 --- /dev/null +++ b/tests/compat/tests/milestone_10_integration.rs @@ -0,0 +1,80 @@ +//! Milestone 10 integration coverage across core primitives and native extensions. + +use libremetaverse::rendering::{DetailLevel, SimpleMesh}; +use libremetaverse::{Primitive, PrimitiveConstructionData}; +use libremetaverse_compat_tests::block_on; +use libremetaverse_compat_tests::rlv_support::RlvHarness; +use libremetaverse_lsl_tools::{Lexer, Parser, generated_lexer, generated_parser}; +use libremetaverse_rendering_mesh_foundry::MeshFoundry; +use libremetaverse_rendering_simple::SimpleRenderer; +use libremetaverse_types::{HoleType, PCode, PathCurve, ProfileCurve}; + +fn world_primitive() -> Primitive { + let mut primitive = Primitive::new_with_constructor().expect("Primitive constructor"); + let mut construction = PrimitiveConstructionData::new_with_constructor() + .expect("PrimitiveConstructionData constructor"); + construction.p_code = PCode::Prim; + construction.path_curve = PathCurve::Line; + construction.set_profile_curve_with_property(ProfileCurve::Square); + construction.set_profile_hole(HoleType::Same); + construction.path_scale_x = 1.0; + construction.path_scale_y = 1.0; + construction.path_end = 1.0; + construction.profile_end = 1.0; + construction.path_revolutions = 1.0; + primitive.prim_data = construction; + primitive +} + +fn mesh_signature(mesh: &SimpleMesh) -> u64 { + let mut hash = 0xcbf2_9ce4_8422_2325_u64; + for vertex in &mesh.vertices { + for value in [ + vertex.position.x, + vertex.position.y, + vertex.position.z, + vertex.normal.x, + vertex.normal.y, + vertex.normal.z, + vertex.tex_coord.x, + vertex.tex_coord.y, + ] { + hash = (hash ^ u64::from(value.to_bits())).wrapping_mul(0x0000_0100_0000_01b3); + } + } + hash +} + +#[test] +fn native_extensions_consume_core_models_and_preserve_service_state() { + let primitive = world_primitive(); + let simple = SimpleRenderer::new() + .expect("simple renderer") + .generate_simple_mesh(primitive.clone(), DetailLevel::High) + .expect("simple mesh"); + let foundry = MeshFoundry::new() + .expect("mesh foundry") + .generate_simple_mesh(primitive, DetailLevel::High) + .expect("foundry mesh") + .expect("legacy primitive is renderable"); + assert_eq!(simple.vertices.len(), foundry.vertices.len()); + assert_eq!(mesh_signature(&simple), mesh_signature(&foundry)); + assert_eq!(simple.indices, foundry.indices); + + let lexer = Lexer::new(generated_lexer().expect("generated lexer")).expect("lexer"); + let mut parser = + Parser::new(generated_parser().expect("generated parser"), lexer).expect("parser"); + let symbol = parser + .parse_with_string("Widget(value;):base(new Child[])".to_owned()) + .expect("generated class body"); + assert_eq!( + (symbol.yyname(), symbol.yynum()), + ("ClassBody".to_owned(), 19) + ); + + let rlv = RlvHarness::new().expect("RLV service"); + assert!(block_on(rlv.process("@fly=n")).expect("add restriction")); + assert!(!rlv.service.permissions().can_fly().expect("permission")); + assert!(block_on(rlv.process("@fly=y")).expect("remove restriction")); + assert!(rlv.service.permissions().can_fly().expect("permission")); +} diff --git a/tools/check_milestone_10.py b/tools/check_milestone_10.py new file mode 100644 index 0000000..30d3ab4 --- /dev/null +++ b/tools/check_milestone_10.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +"""Audit the complete native rendering, RLV, and LSL milestone boundary.""" + +from __future__ import annotations + +import json +import re +import subprocess +from pathlib import Path + +import generate_api_shims + + +ROOT = Path(__file__).resolve().parents[1] +ASSEMBLIES = { + "LibreMetaverse.Rendering.Simple": (1, 6), + "LibreMetaverse.Rendering.MeshFoundry": (1, 14), + "LibreMetaverse.RLV": (28, 499), + "LibreMetaverse.LslTools": (164, 768), +} +CRATES = ( + "libremetaverse-rendering-simple", + "libremetaverse-rendering-mesh-foundry", + "libremetaverse-rlv", + "libremetaverse-lsl-tools", +) +RLV_GENERATED_TYPES = { + "T:LibreMetaverse.RLV.IRlvActionCallbacks", + "T:LibreMetaverse.RLV.IRlvQueryCallbacks", + "T:LibreMetaverse.RLV.RlvAttachmentPoint", + "T:LibreMetaverse.RLV.RlvGestureState", + "T:LibreMetaverse.RLV.RlvGetDebugType", + "T:LibreMetaverse.RLV.RlvGetEnvType", + "T:LibreMetaverse.RLV.RlvPermissionsService.HoverTextLocation", + "T:LibreMetaverse.RLV.RlvPermissionsService.ObjectLocation", + "T:LibreMetaverse.RLV.RlvPermissionsService.TouchLocation", + "T:LibreMetaverse.RLV.RlvRestrictionType", + "T:LibreMetaverse.RLV.RlvWearableType", +} +STUB_RE = re.compile( + r"\b(?:not_implemented|unimplemented_api)\b|\b(?:todo|unimplemented)!\s*\(" +) +PLATFORM_RE = re.compile(r"unsafe\s*\{|unsafe\s+impl|target_os\s*=\s*\"macos\"") + + +def require_markers(path: Path, markers: tuple[str, ...]) -> None: + text = path.read_text() + missing = [marker for marker in markers if marker not in text] + if missing: + raise SystemExit(f"{path}: audit evidence missing: " + ", ".join(missing)) + + +def catalog_assemblies() -> dict[str, dict]: + catalog = json.loads((ROOT / "api" / "public-api.json").read_text()) + return {assembly["identity"]["name"]: assembly for assembly in catalog["assemblies"]} + + +def member_is_native(item: dict, member: dict) -> bool: + doc_id = item["doc_id"] + return ( + doc_id in generate_api_shims.NATIVE_TYPES + or doc_id in generate_api_shims.NATIVE_GENERATED_TYPES + or ( + doc_id in generate_api_shims.NATIVE_DECLARATIONS + and member["kind"] in {"field", "constant"} + ) + or generate_api_shims.native_member_body(member["doc_id"]) is not None + ) + + +def check_catalog_ownership() -> None: + assemblies = catalog_assemblies() + native_types = ( + set(generate_api_shims.NATIVE_TYPES) + | set(generate_api_shims.NATIVE_DECLARATIONS) + | set(generate_api_shims.NATIVE_GENERATED_TYPES) + ) + for name, expected in ASSEMBLIES.items(): + assembly = assemblies[name] + actual = ( + len(assembly["types"]), + sum(len(item["members"]) for item in assembly["types"]), + ) + if actual != expected: + raise SystemExit(f"{name}: expected catalog counts {expected}, found {actual}") + missing_types = [ + item["doc_id"] for item in assembly["types"] if item["doc_id"] not in native_types + ] + missing_members = [ + member["doc_id"] + for item in assembly["types"] + for member in item["members"] + if not member_is_native(item, member) + ] + if missing_types or missing_members: + raise SystemExit( + f"{name}: non-native types={missing_types}, members={missing_members}" + ) + if generate_api_shims.NATIVE_GENERATED_TYPES != RLV_GENERATED_TYPES: + raise SystemExit("milestone 10 generated-native RLV declaration set changed") + + +def check_sources_and_manifests() -> None: + offenders: list[str] = [] + platform_offenders: list[str] = [] + for crate in CRATES: + crate_root = ROOT / "crates" / crate + manifest = (crate_root / "Cargo.toml").read_text() + if "[features]" in manifest: + raise SystemExit(f"{crate}: native implementation must be active without a feature") + if re.search(r"\b(?:dotnet|csc|mcs)\b", manifest, re.IGNORECASE): + raise SystemExit(f"{crate}: manifest depends on a C#/.NET tool") + for source in (crate_root / "src").glob("*.rs"): + text = source.read_text() + if STUB_RE.search(text): + offenders.append(str(source.relative_to(ROOT))) + if PLATFORM_RE.search(text): + platform_offenders.append(str(source.relative_to(ROOT))) + if offenders: + raise SystemExit("milestone 10 owned Rust stubs remain: " + ", ".join(offenders)) + if platform_offenders: + raise SystemExit( + "milestone 10 unsafe or macOS-only sources remain: " + + ", ".join(platform_offenders) + ) + + +def check_parity() -> None: + catalog = json.loads((ROOT / "tests" / "upstream-tests.json").read_text()) + related = [ + test + for test in catalog["tests"] + if test["source"].startswith("LibreMetaverse.Tests/RLV/") + or test["source"].startswith("LibreMetaverse.Rendering.Tests/") + ] + if len(related) != 691: + raise SystemExit(f"milestone 10 expected 691 upstream parity cases, found {len(related)}") + bad = [ + test["id"] + for test in related + if test["status"] != "translated" or test.get("semantic_review") != "reviewed" + ] + if bad: + raise SystemExit("milestone 10 parity cases are not reviewed: " + ", ".join(bad)) + missing = sorted( + { + test["rust_file"] + for test in related + if not (ROOT / test["rust_file"]).is_file() + } + ) + if missing: + raise SystemExit("milestone 10 translated files are missing: " + ", ".join(missing)) + + +def check_contract_evidence() -> None: + require_markers( + ROOT / "crates" / "libremetaverse-rendering-simple" / "src" / "simple_renderer.rs", + ("MAX_FACE_VERTICES", "checked_add", "u16::try_from"), + ) + require_markers( + ROOT / "crates" / "libremetaverse-rendering-mesh-foundry" / "src" / "mesh_foundry.rs", + ("MAX_ASSET_BYTES", "MAX_TOTAL_VERTICES", "MAX_JOINTS", "checked_add"), + ) + require_markers( + ROOT / "crates" / "libremetaverse-rlv" / "src" / "service.rs", + ( + "MAX_RLV_MESSAGE_BYTES", + "MAX_RLV_COMMANDS", + "Arc", + "Arc", + "throw_if_cancellation_requested", + ), + ) + require_markers( + ROOT / "crates" / "libremetaverse-lsl-tools" / "src" / "parser.rs", + ("MAX_PARSER_STATES", "MAX_PARSER_STACK", "MAX_PARSER_STEPS", "MAX_RECOVERY_ERRORS"), + ) + require_markers( + ROOT / "crates" / "libremetaverse-lsl-tools" / "src" / "lexer.rs", + ("MAX_SOURCE_UNITS", "MAX_TOKEN_UNITS"), + ) + require_markers( + ROOT / "tests" / "compat" / "tests" / "milestone_10_integration.rs", + ( + "native_extensions_consume_core_models_and_preserve_service_state", + "SimpleRenderer", + "MeshFoundry", + "generated_parser", + "RlvHarness", + ), + ) + require_markers( + ROOT / "docs" / "extension-milestone-gate.md", + ( + "Choosing an extension", + "Rendering migration", + "RLV host integration and teardown", + "LSL tooling migration", + "194 types", + "1,287 members", + "tools/test_milestone_10.py", + "ubuntu-latest", + ), + ) + require_markers( + ROOT / "api" / "SHIM-COVERAGE.md", + tuple( + f"`{name}` | {types} | {members} | native implementation" + for name, (types, members) in ASSEMBLIES.items() + ), + ) + + +def check_generation_and_ci() -> None: + subprocess.run( + ["python3", "tools/generate_api_shims.py", "--check"], cwd=ROOT, check=True + ) + subprocess.run( + ["python3", "tools/generate_lsl_tables.py", "--check"], cwd=ROOT, check=True + ) + workflow = ROOT / ".gitea" / "workflows" / "rust-workspace.yml" + require_markers( + workflow, + ( + "python3 tools/check_milestone_10.py", + "python3 tools/test_milestone_10.py", + "runs-on: ubuntu-latest", + ), + ) + if re.search(r"runs-on:\s*(?:macos|windows)", workflow.read_text(), re.IGNORECASE): + raise SystemExit("milestone 10 workflow is not ubuntu-only") + + +def main() -> None: + check_catalog_ownership() + check_sources_and_manifests() + check_parity() + check_contract_evidence() + check_generation_and_ci() + print( + "milestone 10 audit: 194 types and 1,287 members are native; no owned " + "stubs remain; bounds, callback teardown, 691 reviewed parity cases, " + "integration, migration docs, deterministic generation, and ubuntu-only CI are present" + ) + + +if __name__ == "__main__": + main() diff --git a/tools/check_milestone_10_issue_81.py b/tools/check_milestone_10_issue_81.py index d2d6c43..7d5be73 100644 --- a/tools/check_milestone_10_issue_81.py +++ b/tools/check_milestone_10_issue_81.py @@ -132,7 +132,7 @@ def main() -> None: raise SystemExit(f"issue 81 expected 27 focused fixtures, found {test_count}") require_markers(DOC, ( "UTF-16 code-unit offsets", "maximum munch", "structured `Diagnostic`", - "64 Mi UTF-16", "16 Mi units", "Lexer::iter", "27 focused", + "64 Mi UTF-16", "16 Mi units", "Lexer::iter", "46 focused", "No C#, .NET runtime", "ubuntu-latest", )) require_markers(WORKFLOW, ("python3 tools/check_milestone_10_issue_81.py",)) diff --git a/tools/check_milestone_10_issue_82.py b/tools/check_milestone_10_issue_82.py index ac39c92..5d620bb 100644 --- a/tools/check_milestone_10_issue_82.py +++ b/tools/check_milestone_10_issue_82.py @@ -122,7 +122,7 @@ def main() -> None: require_markers(EXTENSION_TESTS, ("assert!(YyParser::new().is_ok())",)) require_markers(DOC, ( "canonical LR(0)", "deterministic LALR(1)", "Shift/reduce conflicts", - "nonassociative", "1,048,576", "16,777,216", "37 focused", + "nonassociative", "1,048,576", "16,777,216", "46 focused", "issue 83", "ubuntu-latest", )) require_markers(WORKFLOW, ("python3 tools/check_milestone_10_issue_82.py",)) diff --git a/tools/generate_api_shims.py b/tools/generate_api_shims.py index c0512a5..2a9cac0 100644 --- a/tools/generate_api_shims.py +++ b/tools/generate_api_shims.py @@ -601,6 +601,26 @@ NATIVE_DECLARATIONS = { "T:LibreMetaverse.Rendering.MeshFoundry": "crate::mesh_foundry::MeshFoundry", } +# Catalog declarations which are themselves complete native Rust. Unlike +# NATIVE_TYPES, these types intentionally remain in generated.rs: the RLV +# enums are closed value tables and the callback interfaces are Rust traits +# whose required methods are implemented by the host and default adapters. +# Keeping this set explicit lets coverage distinguish concrete generated code +# from a callable failure-only compatibility shell. +NATIVE_GENERATED_TYPES = { + "T:LibreMetaverse.RLV.IRlvActionCallbacks", + "T:LibreMetaverse.RLV.IRlvQueryCallbacks", + "T:LibreMetaverse.RLV.RlvAttachmentPoint", + "T:LibreMetaverse.RLV.RlvGestureState", + "T:LibreMetaverse.RLV.RlvGetDebugType", + "T:LibreMetaverse.RLV.RlvGetEnvType", + "T:LibreMetaverse.RLV.RlvPermissionsService.HoverTextLocation", + "T:LibreMetaverse.RLV.RlvPermissionsService.ObjectLocation", + "T:LibreMetaverse.RLV.RlvPermissionsService.TouchLocation", + "T:LibreMetaverse.RLV.RlvRestrictionType", + "T:LibreMetaverse.RLV.RlvWearableType", +} + NATIVE_MEMBER_BODIES = { "M:LibreMetaverse.RLV.RlvCommon.TryGetAttachmentPointFromItemName(System.String,System.Nullable{LibreMetaverse.RLV.RlvAttachmentPoint}@)": "Self::native_try_get_attachment_point_from_item_name(item_name, attachment_point)", @@ -3523,9 +3543,18 @@ def native_member_body(member_id: str) -> str | None: return None -def render_body(signature: str, member_id: str, error_model: str, asyncness: str, trait: bool) -> str: +def render_body( + signature: str, + member_id: str, + error_model: str, + asyncness: str, + trait: bool, + required_trait_method: bool = False, +) -> str: if trait: signature = signature.removeprefix("pub ") + if required_trait_method: + return f" {signature};" if body := native_member_body(member_id): if any(f"LibreMetaverse.{owner}." in member_id for owner in CLIENT_CORE_NATIVE_OWNERS): marker = " /* native client-core implementation */" @@ -3647,6 +3676,7 @@ def render_type(item: dict, type_row: dict, member_rows: dict[str, dict[str, str row["error_model"], row["asyncness"], trait, + trait and item["doc_id"] in NATIVE_GENERATED_TYPES, ) ) lines.append("}") @@ -3844,7 +3874,9 @@ def coverage_report(catalog: dict, coverage: dict[str, tuple[int, int, bool]]) - native_type_ids = { item["doc_id"] for item in assembly["types"] - if item["doc_id"] in NATIVE_TYPES or item["doc_id"] in NATIVE_DECLARATIONS + if item["doc_id"] in NATIVE_TYPES + or item["doc_id"] in NATIVE_DECLARATIONS + or item["doc_id"] in NATIVE_GENERATED_TYPES or (name == "LibreMetaverse.Types" and item["kind"] == "enum") } native_member_ids = { @@ -3852,6 +3884,7 @@ def coverage_report(catalog: dict, coverage: dict[str, tuple[int, int, bool]]) - for item in assembly["types"] for member in item["members"] if item["doc_id"] in NATIVE_TYPES + or item["doc_id"] in NATIVE_GENERATED_TYPES or (name == "LibreMetaverse.Types" and item["kind"] == "enum") or ( item["doc_id"] in NATIVE_DECLARATIONS diff --git a/tools/test_milestone_10.py b/tools/test_milestone_10.py new file mode 100644 index 0000000..de05aec --- /dev/null +++ b/tools/test_milestone_10.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Run only tests owned by the rendering, RLV, and LSL milestone.""" + +from __future__ import annotations + +import json +import os +import subprocess +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +ENV = os.environ.copy() +ENV.setdefault("CARGO_BUILD_JOBS", "1") +ENV.setdefault("CARGO_INCREMENTAL", "0") +ENV.setdefault("CARGO_PROFILE_DEV_DEBUG", "0") +ENV.setdefault("CARGO_PROFILE_TEST_DEBUG", "0") +ENV.pop("RUN_LIVE_TESTS", None) + + +def run(command: list[str]) -> None: + print("+", " ".join(command), flush=True) + subprocess.run(command, cwd=ROOT, env=ENV, check=True) + + +run( + [ + "cargo", + "test", + "-p", + "libremetaverse-rendering-simple", + "-p", + "libremetaverse-rendering-mesh-foundry", + "-p", + "libremetaverse-rlv", + "-p", + "libremetaverse-lsl-tools", + "--locked", + "-j", + "1", + ] +) + +catalog = json.loads((ROOT / "tests" / "upstream-tests.json").read_text()) +rlv_targets = sorted( + { + Path(test["rust_file"]).stem + for test in catalog["tests"] + if test["source"].startswith("LibreMetaverse.Tests/RLV/") + } +) +if len(rlv_targets) != 24: + raise SystemExit(f"milestone 10 expected 24 RLV test targets, found {rlv_targets}") + +compat = ["cargo", "test", "-p", "libremetaverse-compat-tests"] +for target in ( + "imaging_meshing_semantics", + "rendering_shims", + "extension_shims", + "milestone_10_integration", + *rlv_targets, +): + compat.extend(("--test", target)) +compat.extend(("--locked", "-j", "1")) +run(compat) + +run( + [ + "cargo", + "check", + "--manifest-path", + "tests/api-compile/Cargo.toml", + "--locked", + "-j", + "1", + ] +)