Track L: Inventory API v3 (AIS) and server-side appearance baking (SSB) - #207
Merged
Merged
Conversation
A standalone server-side bake compositor: it reproduces the Linden Lab viewer's texture-layer composition (avatar_lad.xml layer sets, alpha masks, the 5th "morph mask" component, single-tile J2K encode) so the simulator can bake an avatar without the viewer. Ships avatar_lad.xml and 56 character TGAs as embedded data resources. Their origin, licence (LGPL 2.1 with the Linden Lab viewer linking exception), viewer version (26.1.1) and per-file SHA-256 are recorded in THIRD-PARTY-NOTICES.md. The golden harness compares our output against reference viewer bakes. The fixtures are NOT committed (they are avatar data from a live grid); each set degrades to a printed SKIPPED line with the fetch instructions when its fixtures directory is absent, and asserts nothing. That is why the suite is green in a fresh checkout. Folded from feature/ais-v3, 15 commits: 564ca61 feat(ssb): OpenSimNGC.Appearance.Baking skeleton with embedded avatar_lad.xml 2142706 feat(ssb): compositor moved into OpenSimNGC.Appearance.Baking; single-tile J2K; BakeHash e3b969c test(ssb): golden harness against Truly's Firestorm bakes (fixtures fetched, not committed) 2e5c960 fix(ssb): J2K decode treats 5-component viewer bakes as RGBA (bump channel dropped) 6f52f8b docs(ssb): ledger P-1 (LL compositor is the authority), Q-7 resolved, Q-8/Q-9 opened, S0b logged; harness wording: reference bakes 203ec18 feat(ssb): bump (5th component) pass per LL lltexlayer; masks re-verified against viewer tree 8835f48 fix(ssb): render no-texture layers once per worn wearable instance (LL template semantics) 0176eed docs(ssb): 5th component is the morph mask — rename, cite packing order bbc065b test(ssb): golden RGB/A thresholds; docs: ADR-007 masks, packing-order citation 296051b test(ssb): multi-set golden harness + aleric-max reference set ade0e01 fix(ssb): upper morph mask gathers from the top worn wearable, asset or not e338ecc feat(ssb): library reports a channel where nothing was drawn 3455def test(ssb): close S1 DoD — golden diff at the shipped bake size 86880b7 feat(ssb): persist bakes and skip recompute on unchanged inputs 162bfad perf(ssb): instrument the bake phases and answer Q-10
AISv3Module registers the InventoryAPIv3 and LibraryAPIv3 caps behind a config flag and serves the viewer's AIS v3 surface: fetch (item/category/COF, depth contract taken from the viewer's own fetch path), item and category mutations with delta envelopes, SlamFolder with a compensating restore, purge descendents, and CopyLibraryCategory. Supporting service work: IInventoryService gains a folder-delete overload that can delete outside Trash (implemented across XInventoryService, the local and remote connectors, HGInventoryBroker, the HG inventory services and the XInventory in-connector); system folders are resolved deterministically rather than by folders[0]; the duplicate-system-folder race is narrowed. Removes the seven-file OpenSim.Services.AISv3 ASP.NET webapi template (WeatherForecast scaffolding) that the region module replaces. Docs/feature/ais-v3 carries the spec extracted from the viewer, the A5 live checklist, the malformed-LLSD audit and the A6-A13 investigation records. Folded from feature/ais-v3, 76 commits: 133c3e3 docs(ais): AIS v3 spec extracted from viewer + tree state 8aa8104 feat(ais): AISv3Module skeleton, router, envelope fixtures; remove webapi template 03e428c feat(ais): inventory backend for the fetch surface 9be9d2e feat(ais): fetch routes + LibraryAPIv3 (read-only) 9d43eb2 test(ais): HTTP acceptance harness for the fetch surface 4d9a661 docs(ais): A-R3 refined, A-Q6 opened, orphans scope recorded f8bfaa2 feat(ais): item and category mutations with delta envelopes 72ee7be test(ais): mutation harness d5afbce docs(ais): depth contract settled from the viewer's fetch path e2ad97a feat(ais): folder deletion outside Trash via a new IInventoryService overload 2bf8ed8 fix(ais): protected-folder set taken from the viewer's own table 193d7e6 feat(ais): SlamFolder with compensating restore 664608d test(ais): slam and create harness, fault injection 20ab17f fix(ais): create body shapes taken from the viewer's builders d18897e feat(ais): purge descendents ac5a43e feat(ais): CopyLibraryCategory 295884e test(ais): purge and copy harness 6fc4ab3 fix(ais): categories create shape from asAISCreateCatLLSD 4d725a7 feat(ais): advertise InventoryAPIv3 and LibraryAPIv3 behind the region flag 3448d6e docs(ais): A5 live checklist fa4eb5f fix(ais): register the caps as variable-path handlers ee6d2a3 docs(ais): A6 and A5 ledger rows, risk A-R7 6cd13a3 fix(ais): resolve system folders deterministically, not by folders[0] 68bfa60 fix(inventory): narrow the race that creates duplicate system folders e800eb6 docs(ais): fill the A6 and A7 ledger commit hashes 4165077 docs(ais): A5 checklist run 2026-09-04 — results and constraints 455b28e docs(ais): fill the A8 ledger commit hash 80e2348 docs(ais): step 8 passed; step 13 fallback confirmed server-side 3fb35f6 docs(ais): A9 - no duplicate COFs exist; the pattern is the HG suitcase skeleton, correcting A7-DUPLICATE-COF.md 96a8bad docs(ais): fill the A9 ledger commit hash dd90969 docs(ais): A10 - step 10 reopened, no cause established 40a1438 docs(ais): fill the A10 ledger commit hash f1d32d6 docs(ais): A-Q15 closed - the item-create fallback is LL's, not Firestorm's 25ce8b0 feat(ais): log mutation response deltas at DEBUG b18bc40 docs(ais): A12 design brief - attachment reconciliation, and the real cause of step 10 6d5475d docs(ais): A13 - step 10 closed; A-Q17 answered; T-2 deployed 7e023e8 docs(ais): fill the A13 ledger commit hash 15bb1c5 docs(ais): fill the A14 ledger commit hash 2697af6 docs(ais): step 7 is not viewer-reachable; Robust deploy verified live 6077664 docs(ais): fill the A15 ledger commit hash 1ea8efa docs(ssb): S3 recorded — Q-14 closed, Q-16 ruled, Q-6 now measurable 1a7dce2 test(ais): reproduce A16 — UpdateItem answers 200 and drops asset_id 159fd41 fix(ais): UpdateItem persists the asset, the permissions and the version cb27bc9 docs(ais): A18 ledger row, spec §1d-ter, checklist step 16 66fc104 docs(ais): record the A18 deploy of both roots 0b8abad docs(ais): repair a stray newline in the A18 deploy row's backup path 6b2a33b docs(ais): do not call the attribution correction a "P-1 session" f9dee45 feat(ais/ssb): editing a worn wearable now rebakes that channel 05c4e08 docs(ssb): S9 ledger row, the trigger table, checklist step 17 77050c5 docs(ssb): S10 Part 5 — trigger table, Q-11 live results, step 18 508ebc1 fix(ais): a refused wearable save is a 403, and library textures are legal bb4bcd0 feat(config): S12 — one switch for the grid, and say which config decided 373bdcb test(ais): adversarial cross-user cases for the real backend f8263e2 fix(ais): owner-scope InventoryServiceBackend (AIS-SEC-1) 0cbd371 docs(ais): record AIS-SEC-1 16f75c1 docs(ais): step 19 verified bd5cfe0 test(ais): malformed and invalid SLAM bodies must not write 97c4ff0 test(ais): mutation harness sends non-map bodies instead of nulling them 6c37b5e fix(ais): fail closed on malformed bodies, strict SLAM validation (AIS-SEC-2) 9ea6cdb docs(ais): record AIS-SEC-2 d0dd427 docs(ais): step 20 verified 3057327 fix(inventory): serialize CreateUserInventory per principal, warn on root duplicates (AIS-COF-1) 5f3bf17 docs(ais): record AIS-COF-1 d5ba0f3 docs(ais): step 21 verified ffdd386 test(ais): concurrent mutations on one folder must serialize 24fedc5 fix(ais): per-(agent,folder) mutation lock for slam/purge/create (AIS-SEC-3) 3e1da28 docs(ais): record AIS-SEC-3 5ff1787 docs(ais): step 22 verified 57f5e0d test(ais): CreateInventory must report partial creates on failure 0386575 fix(ais): report partially created objects when CreateInventory fails (AIS-SEC-4) 73c8933 docs(ais): record AIS-SEC-4 7a89501 docs(ais): record AIS-AUDIT-1 (malformed-LLSD blind spot, tree-wide) c7c2b5d docs(env): record ENV-1 863af77 test(ais): error hygiene and cyclic folder traversal d567a72 fix(ais): log exceptions server-side, guard traversal with a visited set (AIS-SEC-5) 9402cf6 docs(ais): record AIS-SEC-5
…ice, and wire protocol The simulator composes an avatar's bake itself instead of relaying the viewer's. Region side (OpenSim.Region.OptionalModules/Avatar/ServerSideBaking): ServerSideBakingRegion sets RegionProtocols bit 0 and registers the UpdateAvatarAppearance cap; CofHandshake implements the viewer's POST cof_version / success|error|expected contract; BakeOrchestrator drives OpenSimNGC.Appearance.Baking and persists results; BakeIndex keeps the Bake:<channel> index on the avatar record so a bake survives an appearance save. ScenePresence sends the AppearanceData block with appearance_version 1, and an NPC's appearance no longer claims a server bake it does not have. Robust side: AppearanceService + AppearanceServerConnector serve GET texture/<agent>/<channel>/<uuid>, and LLLoginService advertises agent_appearance_service in the login response (empty by default, which omits the key and leaves the viewer compositing locally). Also carried here, because the bake reads what the appearance path stores: the appearance-integrity fixes (AgentIsNowWearing no longer wipes unlisted wearable slots; a refused wearable save is a 403; library textures are legal), the [AIS]/[Appearance] global config switch (S12), the env/LightShare fixes, and the NUnit-lifecycle test fixtures orphaned by the xunit migration. Tranquillity.sln gains the five Track L projects via `dotnet sln add`. Folded from feature/ais-v3, 28 commits over these paths: c1fc7ff test(fixtures): NUnit lifecycle hooks orphaned by the xunit migration — CoreModules.Tests 35→5 failing d43f8cb test(caps): LindenCaps.Tests restored to the solution 11a2456 fix(appearance): AgentIsNowWearing no longer wipes unlisted wearable slots 564ca61 feat(ssb): OpenSimNGC.Appearance.Baking skeleton with embedded avatar_lad.xml 8aa8104 feat(ais): AISv3Module skeleton, router, envelope fixtures; remove webapi template 99118ea feat(ssb): ServerSideBakingModule — orchestrator + console serverbake 7c972b7 fix(ssb): orchestrator keeps worn-but-assetless wearable slots 9b428e0 fix(ssb): never store or apply a bake for a channel with nothing drawn 4d725a7 feat(ais): advertise InventoryAPIv3 and LibraryAPIv3 behind the region flag dc4e417 fix(appearance): flush a pending appearance save when the presence closes 86880b7 feat(ssb): persist bakes and skip recompute on unchanged inputs 162bfad perf(ssb): instrument the bake phases and answer Q-10 1e78b9a fix(ssb): an appearance save no longer wipes the bake index 7554bf9 feat(ssb): UpdateAvatarAppearance cap, AppearanceData, RegionProtocols bit 0 59b1253 feat(ssb): Robust appearance service for baked textures 51194ac feat(ssb): advertise agent_appearance_service in the login response eda2681 fix(ssb): send appearance_version param 1 with the AppearanceData block d7ac58d feat(ssb): rebake on wearable change 9417a09 test(ssb): BoM aux channels end to end b13f15a docs(ssb): withdraw the unsourced Q-6 "310 ms" measurement 483c2d7 fix(appearance): keep unresolvable wearables, never save from a child, refuse a bake that lost a body part 73d25e3 feat(ssb): keep every COF link of a type, in the viewer's "@n" order b0b3c6b fix(appearance): persist wearable types 15 and 16, and record S11 bb4bcd0 feat(config): S12 — one switch for the grid, and say which config decided 162e117 fix(config): put S12's OpenSim.ini.example block in the right place dc0b7c0 fix(ssb): SSB-NPC-1 - an NPC's appearance must not claim a server bake it does not have c65cf09 test(env): a non-array WindLight body must not reach DayCycle f6981e1 fix(env): guard on the cast result, not the parameter (ENV-1)
The design record behind the AIS v3 and server-side-baking lanes: the SSB recon and its addendum, the design brief, the ADR set, the SSB and combined Track L build plans, the S0a verification pass, the S1b fidelity surface, and the fact sheet written for review. Also the two repo-audit notes the lanes depend on: S8-WEARABLE-RESOLUTION (where the appearance save actually runs) and T1-TEST-FIXTURES (the NUnit lifecycle hooks orphaned by the xunit migration, and the LindenCaps.Tests restoration). The Legion-only operational records — the two lane ledgers, KnownDefects, the commit inventory and the R1 Robust reconciliation — are deliberately not carried; they record a specific grid's deployments, not this work. Folded from feature/ais-v3, 36 commits over these paths: c1fc7ff test(fixtures): NUnit lifecycle hooks orphaned by the xunit migration — CoreModules.Tests 35→5 failing d43f8cb test(caps): LindenCaps.Tests restored to the solution 2720a48 docs(ssb): recon addendum, design brief, ADR set, build plans (SSB + Track L combined), ledger, Mike fact sheet 6534668 docs(ssb): recon addendum, design brief, ADR set, build plans (SSB + Track L combined), ledger, Mike fact sheet 5ed0d1e docs(ssb): S0a verification pass c90de5e docs(ssb): ledger — Q-2/Q-3/Q-4 resolved, S0a/S0c logged 6f52f8b docs(ssb): ledger P-1 (LL compositor is the authority), Q-7 resolved, Q-8/Q-9 opened, S0b logged; harness wording: reference bakes 73b842c docs(ssb): ledger — Q-8 resolved in S0d, session logged 0176eed docs(ssb): 5th component is the morph mask — rename, cite packing order bbc065b test(ssb): golden RGB/A thresholds; docs: ADR-007 masks, packing-order citation 2b6d9bb docs(ssb): S1 done — first live server bake verified; Q-9 closed, Q-10/Q-11 opened; build plan actuals 2f85db3 docs(ssb): S1b fidelity surface, bake-size comparison, Q-11 status 2a916c7 docs(ssb): Q-12 resolved, S1c logged ab7e06c docs(ssb): ADR-008 default is 1024, superseding D-7 by measurement e39cfe4 docs(ssb): Q-13 resolved, S1c/S1d/S1e logged 2697af6 docs(ais): step 7 is not viewer-reachable; Robust deploy verified live 3455def test(ssb): close S1 DoD — golden diff at the shipped bake size 86880b7 feat(ssb): persist bakes and skip recompute on unchanged inputs 162bfad perf(ssb): instrument the bake phases and answer Q-10 95c3eef docs(ssb): the AIS/COF seam for S5; S2 live measurement 1ea8efa docs(ssb): S3 recorded — Q-14 closed, Q-16 ruled, Q-6 now measurable 26dc949 docs(ssb): S4 done; the S3 flag flip and why the cloud was correct db64e6f docs(ssb): S5 recorded — Q-6 answered, trigger and aux channels e2060d3 docs(ssb): §4.3 states the cap's new contract; S5 trigger breadth recorded b13f15a docs(ssb): withdraw the unsourced Q-6 "310 ms" measurement bcba73d docs(ssb): S6 recon — Q-5 answered, LibreMetaverse surfaces everything needed 78ffbd3 docs(ssb): S5 live verify — ten COF mutations, ten bakes, paired ba7f0af docs(ssb): the gateway's own bake is overwritten on a bit-0 region b85b7b3 docs(ssb): ADR-009 accepted, S6(b) ledger row 05c4e08 docs(ssb): S9 ledger row, the trigger table, checklist step 17 77050c5 docs(ssb): S10 Part 5 — trigger table, Q-11 live results, step 18 b0b3c6b fix(appearance): persist wearable types 15 and 16, and record S11 faf28cb docs(ssb): Q-11 — the verbatim bake line, and gloves/skirt were seen live 508ebc1 fix(ais): a refused wearable save is a 403, and library textures are legal bb4bcd0 feat(config): S12 — one switch for the grid, and say which config decided dc0b7c0 fix(ssb): SSB-NPC-1 - an NPC's appearance must not claim a server bake it does not have
…rizer reference
NOT PART OF THE TRACK L CARVE. Kept as the tip commit so it can be dropped with
`git reset --hard HEAD~1` if your CI can resolve the package.
Source/Warp3D/Warp3D.csproj carries
<PackageReference Include="OpenMetaverse.Rendering.Meshmerizer" Version="1.0.6" />
which resolves from neither configured source: nuget.org returns 404 and the
OpenSim-NGC GitHub Packages feed returns 403 for a token that reads every other
package in the tree. NuGet fails the whole-solution restore on it, so
`dotnet build Tranquillity.sln` produces 19 NU1301 errors on develop before any
of this branch's code is considered.
It is also redundant: Directory.Build.props already gives every project
UtopiaSkye.OpenMetaverse.Rendering.Meshmerizer 1.1.7, the same component from
the package this repo actually publishes. Docs/feature/ssb-appearance/
S0a-VERIFICATION.md recorded it at the time as "likely an upstream slip".
With the line removed, restore succeeds and the solution builds 0 errors.
Two Track L tests carried a live grid's identity as a literal. LoginResponseAdvertisementTests asserted on `http://legiongrid.ddns.net:8002/` as the advertised appearance-service URL; it is now `http://example.org:8002/`. The value is arbitrary to the test, which only checks that what was configured is what the login response carries, in both the XML-RPC and LLSD shapes. AisErrorHygieneTraversalTests injects a connector fault whose text stands in for a real one, and then proves none of it reaches the client. The injected string was a real host and database name; it is now `Server=db.example.org;Database=opensim;Uid=root;Pwd=hunter2`. The assertion that a host address does not reach the wire was updated to match. `Pwd=hunter2` is deliberate: it is the token the test greps for on the wire (must be absent) and in the log (must be present, so the operator still gets the real exception), and it is not a credential to anything.
…onment; absent fixtures are a real skip
Three things made the golden harness unusable by anyone but the grid it was
written on, and made a checkout without fixtures look greener than it was.
fetch-fixtures.sh had one grid's identity as its defaults: a database container
name, a database name, a Windows path to the file holding the root password,
the key name inside that file, a simulator asset-cache path and a localhost
Robust URL. Every one is now a required variable (GOLDEN_DB_CONTAINER,
GOLDEN_DB_NAME, GOLDEN_DB_ENV_FILE, GOLDEN_DB_PW_KEY, GOLDEN_ROBUST_ASSETS,
GOLDEN_REGION_CACHE) declared with ${VAR:?...}, so the script names what is
missing instead of silently reaching for a grid that is not there. A header
block documents each one and gives a worked example with placeholder values.
The logic is unchanged: exporting the six values makes it behave exactly as it
did.
Both manifest.json files named the capture grid by database container and
Robust URL. That field now describes where the capture came from and points at
the script's header for the reader's own grid. Avatar names, asset UUIDs and
the reference bake ids stay: they are the fixtures' identity and the script
reads them.
Eight fixture-dependent tests across two projects returned early after printing
a SKIPPED line, so a run with no fixtures reported eight passes that had
asserted nothing. They are now skipped for real. xunit here is 2.9.3, which has
no per-row skip for a [Theory], so each [Theory]/[MemberData] gate became one
named method per reference set carrying a skip attribute whose constructor sets
Skip when that set's fixtures/avatar.json is absent, with a reason telling an
operator how to generate fixtures from their own grid. No new package
dependency.
Source/OpenSimNGC.Appearance.Baking.Tests GoldenFactAttribute
Tests/...ServerSideBaking.Tests GoldenFixturesFactAttribute
The two are duplicated rather than shared because the second project references
the baking library, not its test project. Reference sets are now named rather
than discovered, which is the point: a set whose fixtures are missing is
visible as a skip instead of vanishing from the run.
BackendTests' transparent-texture test asserted a synthetic case and then the
same rule on real fixtures. It is split, so the synthetic half still runs
everywhere and only the real-content half skips.
Baking.Tests 35 passed / 0 skipped -> 31 passed / 5 skipped / 36
ServerSideBaking.Tests 118 passed / 0 skipped -> 115 passed / 3 skipped / 118
The 36 is the same 35 cases plus the split.
The carve brought across every document the two lanes produced, including the
ones that only make sense to the grid they were written on. The rule applied
here: a document stays if it tells a maintainer or another operator how the
feature works, why it is shaped the way it is, or how to verify a deployment.
A document that narrates one grid's session history goes.
Dropped (they remain on the branch this was carved from):
A5-RUN-2026-09-04, A6-LIVE-FAILURE, A7-DUPLICATE-COF, A9-SUITCASE-NOT-DUPLICATE,
A10-STEP10-REDIAGNOSIS, A12-ATTACHMENT-RECONCILIATION, A13-STEP10-CLOSED
one grid's live runs and the diagnosis threads that closed them. A7 was
itself corrected by A9, so it would have shipped a conclusion the next
document withdraws.
S0a-VERIFICATION, RECON-ssb-appearance, S1b-FIDELITY
a branch-versus-upstream reconciliation pass, the first recon (its
findings are restated in the addendum, which stays) and one session's
fidelity measurements.
BUILD-PLAN-ssb-appearance, BUILD-PLAN-track-L-combined
schedules: session estimates, branch and worktree layout, deploy order.
Neither describes the architecture; the design brief and the ADR set do.
Kept: AIS-V3-SPEC, A5-LIVE-CHECKLIST, AUDIT-1-MALFORMED-LLSD, ADR-SET,
DESIGN-BRIEF, RECON addendum (the V1-V7 viewer contract the whole design hangs
off), FACT-SHEET, S8-WEARABLE-RESOLUTION, T1-TEST-FIXTURES, MORPH-MASK-PASS,
THIRD-PARTY-NOTICES.
Nine code and test comments cited a dropped document. None had its content
deleted: each now states the substance it was pointing at. The A7 citations
mattered most, because that document proposed a dedupe its own successor ruled
out - the comments now say what an operator should actually do, and the
XInventory warning no longer sends them to a file that is not there.
Kept documents lost the paths of one machine: a grid root, two worktrees, a
separate repository's checkout and the reference trees. The viewer source is
named by version rather than by drive letter, which is the part a reader can
act on. Section 7.4 of the design brief went with them - it recorded which
worktree another repository's project reference pointed at, which is true of
nobody else's checkout.
mdickson
approved these changes
Sep 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the two services the current Linden viewer assumes and OpenSim has never had: Inventory API v3 (AIS) and server-side baking (SSB). Both are off by default and each is enabled by one global config line. With both on, the stock Second Life viewer logs in, is textured from the simulator's bakes, and can change outfits, delete, and empty trash - none of which it can do against OpenSim today, because it has no non-AIS path for them. Firestorm works in both modes. Verified with the stock LL viewer 26.3.0 and Firestorm 7.2.5; the protocol was extracted from the LL viewer 26.1.1 source.
223 files changed, 40631 insertions, 231 deletions, in 8 commits. 133 original commits are folded into 4 by path for review navigation (library, AIS, SSB, docs); the commit bodies list what each one folds. Roughly half the insertions are the baking library commit, which includes the vendored
avatar_lad.xmland its tests. Three more commits are hygiene done while preparing the PR (neutral test literals, a portable golden harness, docs triage). One commit (b1da081d1e) is the one-line Warp3D build fix that is also offered standalone as #206: it is here because this branch cannot be restored or built outside the org without it, and the two merge clean in either order.What is in it
OpenSimNGC.Appearance.Baking- a standalone compositor library, a port of the LL viewer's texture-layer rules (lltexlayer/llavatarappearance), emitting 5-component J2C (R,G,B,A plus the morph mask). No simulator dependencies; a second consumer (a browser-viewer gateway that logs in as a client) already uses the same library.AIS v3 - region-side caps module (
InventoryAPIv3, andLibraryAPIv3read-only) overIInventoryService, behindIAisInventoryBackendso the same handler can later be hosted on Robust. All 15 operations the viewer uses, extracted with file:line citations intoDocs/feature/ais-v3/AIS-V3-SPEC.md.SSB - region module that composites from the agent's wearables and visual params, stores bakes as assets with an input hash (an unchanged outfit re-uses its bakes: measured cold about 2.8 s for five channels at 1024, warm about 70 ms), advertises RegionProtocols bit 0, sends the
AppearanceDatablock, serves theUpdateAvatarAppearancecap, rebakes on COF change and on an edit to a worn wearable; plus a Robust appearance service andagent_appearance_servicein the login response.Enabling it
Region side. These are global: a region section may opt OUT, and is never how you opt in.
A single region opts out with
AIS_Enabled = falseorServerSideBaking = falsein its own[<Region Name>]section. The per-region AIS key isAIS_Enabled, notEnabled, because a region section holds settings for many modules. Both modules log one INFO line per region at load naming which config decided, for exampleregion Ebony: AIS v3 ON (global).Robust side. In
[ServiceList]:then the service itself:
and in
[LoginService]:It must reach the connector's port and end with
/, because the viewer appendstexture/...to it with no separator. Left unset, the login response omitsagent_appearance_serviceentirely and the viewer keeps compositing locally, which is correct for a grid with no such service. The same key exists inconfig-include/StandaloneCommon.ini.example.Defaults:
[Appearance] BakeSize = 1024(512, 1024 or 2048) and[Appearance] BakeQuality = 0.85. ChangingBakeSizeinvalidates every stored bake, which is deliberate.[AIS] Enabledand[Appearance] ServerSideBakingboth default tofalse.Operators should know: enabling AIS routes ALL of a capable viewer's inventory traffic through it; there is no viewer-side fallback, which is why the full operation surface ships together.
Changes to existing behaviour - please look at these first
These are fixes to code that is in develop today. SSB depends on several of them, which is why they are here rather than in separate PRs. The commit ids cited below are from the original branch (
feature/ais-v3on my fork, which is public); they do not exist in this folded branch, but the folded commit bodies list them and the file:line proofs are against this branch.Test lifecycle hooks orphaned by the xunit migration. The migration (Feature/xunit tests #197) turned subclasses'
[SetUp]methods into overrides ofSetUp()and nothing invoked them, so every test relying on a field assigned in setup ran against null.OpenSimTestCasenow implementsXunit.IAsyncLifetimeand callsSetUp()fromInitializeAsync.CoreModules.Testswent from 35 failures to 5. Commitc1fc7fff3e. Proof:Tests/OpenSim.Tests.Common/OpenSimTestCase.cs:43.AgentIsNowWearingwiped unlisted wearable slots. The handler replaced the whole wearable array with what the message listed, so any slot the viewer did not mention was cleared. It now merges: unlisted slots are retained,UUID.Zerostill clears, and the appearance is saved only on an actual change. Commit11a2456833. Proof:AvatarFactoryModule.cs:1360declaringMergeNowWearing, withAvatarFactoryNowWearingTests.An appearance save wiped the bake index.
AvatarService.SetAvatardeletes every row for the principal before rewriting the appearance-derived keys, and a save fires about five seconds after any outfit change - so every outfit change destroyed the stored bake index and the next bake recomputed all five channels. The delete-all is load-bearing and stays; a preserved namespace was added instead. Commit1e78b9a706. Proof:Source/OpenSim.Services.Interfaces/IAvatarService.cs:118declaringAvatarDataKeys, andAvatarService.cs:111testingAvatarDataKeys.IsPreserved(name). A test pins that a grid which has never baked is byte-identical to before.A save from a child presence corrupted the stored appearance. The save resolves items against the region it runs on and then writes to the avatar service; on a child presence that is a write about an avatar another region owns, made from a copy. Observed losing skin, hair, eyes and shirt. A child presence now never writes, and a wearable that cannot be resolved keeps its slot instead of being reset to default. Commit
483c2d7a13. Proof:AvatarFactoryModule.cs:893(the child-agent write barrier) and:965(keep the slot). Mechanism written up inDocs/feature/repo-audit/S8-WEARABLE-RESOLUTION.md.Wearable types 15 (Physics) and 16 (Universal) were never persisted. The Avatars writer stopped at the legacy bound of 15 wearables, so those two types were dropped on every save even though the wearable table is 17 wide. Commit
b0b3c6b671. Proof:IAvatarService.cs:199-203.An appearance save was dropped if the agent left inside the save delay.
QueueAppearanceSavedefers byDelayBeforeAppearanceSaveseconds and the handler resolves theScenePresenceonly when the timer fires; if the agent had gone, the write was skipped silently and the change died with the presence. A detach followed by a logout inside the window left the stored appearance still wearing the garment. Commitdc4e417bb3. Proof:AvatarFactoryModule.cs:85subscribingFlushAppearanceSaveOnClosetoOnRemovePresence.Duplicate Textures folders, created deterministically.
XInventoryService.GetSystemFoldersfilteredf.type > 0, which excludes type 0 - Textures - so everyCreateUserInventorycall saw Textures as missing and created another one. This is not a race: it happens on every call, and one account here had accumulated eight. The filter is now>= 0. Separately,CreateUserInventorycan be entered concurrently for the same principal (it is reachable remotely throughXInventoryInConnector) and two overlapping calls can both read "missing" and both create, with no unique key to catch the loser; it is now serialized per principal. Duplicates directly under the root are reported at WARN at login, because nothing self-heals them; suitcase folders are excluded, being legitimate duplicates by type. Commits3057327361and68bfa60735. Proof:XInventoryService.cs:329now filtersf.type >= 0where develop hasf.type > 0at its:202;:116takes the per-principal lock;:266isWarnOnDuplicateSystemFolders.ViewerEnvironment.FromWLOSDtested the parameter, not the cast result. A non-arrayOSDis still non-null, sonullreachedDayCycle.FromWLOSD(OSDArray)and threw on its first statement. That throw was load-bearing: the legacy WindLight setter calledStoreOnRegionimmediately afterwards, so fixing the guard alone would have turned a crash into a silent write of a fresh default environment over the region's real one. Both were changed together - the guard tests the cast result, and the setter now refuses a non-array body at the boundary. Commitf6981e12f9. Proof:Source/OpenSim.Framework/ViewerEnvironment.cs:113-114andEnvironmentModule.cs:758, withViewerEnvironmentWLOSDTests.Wire and interface changes:
IInventoryServicegainsDeleteFolders(UUID userID, List<UUID> folderIDs, bool onlyIfTrash)alongside the existing two-argument form, which is untouched, as is every existing caller. The LL viewer removes any non-protected folder wherever it sits, while the old signature silently skipped any folder whose parent was not Trash or Lost And Found and still returned true. Implemented across all implementors includingXInventoryService,HGInventoryServiceandHGSuitcaseInventoryService. On the Robust wire the flag is carried as an optionalONLYIFTRASHfield that is sent only when it is false, so it is backward-compatible in both directions: an older Robust that does not know the field keeps today's behaviour, and a newer Robust defaults it to true for an older simulator. Commite2ad97af70. Proof:Source/OpenSim.Services.Interfaces/IInventoryService.cs:121and:135;XInventoryInConnector.cs:464readingONLYIFTRASH;XInventoryServicesConnector.cs:395sending it only when false.Security
AIS was put through an external source audit before this PR; all findings are fixed with red-first tests: owner-scoped backend (a valid cap plus another resident's UUID answered 200 before; now a uniform 404), fail-closed body parsing with a 1 MiB ceiling (a malformed slam used to empty the COF), per-(agent, folder) mutation lock, partial-create reporting, generic 500s with server-side logging, and a visited-set on folder traversal.
Docs/feature/ais-v3/AUDIT-1-MALFORMED-LLSD.mdis a tree-wide sweep of client-facing LLSD parse sites for the same anti-pattern: 86 parse sites examined, 40 of them client-facing, 0 destructive. One latent defect one edit away from destructive is recorded in its section 5, and it is theViewerEnvironmentfix above.Tests
CoreModules: the five failures are pre-existing on develop and unrelated -
AvatarFactoryModuleTests.TestSetAppearance,AvatarFactoryModuleTests.TestSaveBakedTextures,InventoryArchiveLoadTests.TestLoadIarCreatorAccountPresent,InventoryArchiveLoadTests.TestLoadCoalesecedItem,InventoryArchiveLoadTests.TestLoadIarV0_1AbsentCreator. Before the lifecycle-hook fix in this PR that project had 35.Golden-image tests compare the compositor against reference bakes captured from the LL compositor. The fixtures are real avatar assets and are not in the repo; without them those eight tests SKIP, with a reason naming the script and the variables it needs.
Golden/fetch-fixtures.shgenerates them from any grid - see its header for the six required environment variables and a worked example.Not carried: one NPC appearance test that lives in a Phlox test project develop does not have.
Verified live
Running grid-wide on a three-region hypergrid-enabled grid (two 256 m regions and one 1024 m var region) since 2026-09-07. Firestorm 7.2.5 on all three regions; the stock LL viewer 26.3.0 on a 256 m region (it cannot enter var regions). Real-content coverage: every wearable type including tattoo, alpha, universal, physics, layered multiples per type, and the BoM aux channels.
Docs/feature/ais-v3/A5-LIVE-CHECKLIST.mdis the operator verification script.Known limitations (all with the flags ON; nothing changes with them off)
POSTitems-create answers 501 by design: the viewer's own builder for it is compiled out (USE_AIS_FOR_NCis never defined), so no stock viewer sends it. The handler refuses the whole body before writing anything rather than creating an item with no asset behind it.SlamLinks). Links are created before the old ones are removed, so a folder never holds fewer links than it started with, but between the last create and the last remove a reader racing the slam sees both sets. The per-(agent, folder) mutation lock closes that within one region process only. Likewise theCreateUserInventorylock is per Robust process.InventoryFolderBase.Versionisushortagainst anint(11)column, so it wraps past 65535 and any version comparison then picks wrongly. Pre-existing; AIS makes it more visible because its resolution rule reads that value.>= 0) has no unit test:Tests/OpenSim.Services.InventoryService.Testsis outside the solution and does not build (NU1605 package downgrades againstOpenSim.Tests.Common). Resurrecting it is a follow-up.(agentID, type)toinventoryfolders: HG suitcase folders are legitimate duplicates by type, andHGSuitcaseInventoryService.CreateSystemFoldersbuilds a full set of them. A candidate shape is(agentID, parentFolderID, type), not settled and not recommended yet.AvatarServices = RemoteAvatarServicesConnector, so the bake-index preservation runs inside Robust: that fix is live only after Robust is redeployed, not after a region-only deploy.RegionProtocolsfromRegionHandshaketo a gateway is still open; it matters only to non-viewer clients.Third-party content
Source/OpenSimNGC.Appearance.Baking/THIRD-PARTY-NOTICES.md:avatar_lad.xml(LL viewer 26.1.1, LGPL 2.1 with the Linden linking exception, unmodified, SHA-256 recorded) and 56 parameter-mask TGAs, each with a SHA-256 verified byte-for-byte against the viewer tree. The notice also records that the viewer commit id named in the design docs could not be confirmed from the checkout used and is a claim rather than a fact.Two decisions I would like from you
Library placement:
OpenSimNGC.Appearance.Bakingis in-tree today (ADR-003). It has no simulator dependencies and a second consumer already exists, so it is a candidate for an NGC NuGet package. Your call; nothing in this PR prevents either.AIS hosting: this is Phase 1, region-side.
IAisInventoryBackendis the seam for hosting the same handler on Robust ("inventory out of the simulator"). I would like your view on where that should live before I build it.Related: #206 is the Warp3D restore fix on its own. The identical change is commit
b1da081d1ehere; whichever merges second is a no-op.