Skip to content

Track L: Inventory API v3 (AIS) and server-side appearance baking (SSB) - #207

Merged
mdickson merged 8 commits into
OpenSim-NGC:developfrom
JohnLegionH:pr/track-l-ais-ssb
Sep 21, 2026
Merged

mdickson merged 8 commits into
OpenSim-NGC:developfrom
JohnLegionH:pr/track-l-ais-ssb

Conversation

@JohnLegionH

Copy link
Copy Markdown

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.xml and 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

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

  2. AIS v3 - region-side caps module (InventoryAPIv3, and LibraryAPIv3 read-only) over IInventoryService, behind IAisInventoryBackend so the same handler can later be hosted on Robust. All 15 operations the viewer uses, extracted with file:line citations into Docs/feature/ais-v3/AIS-V3-SPEC.md.

  3. 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 AppearanceData block, serves the UpdateAvatarAppearance cap, rebakes on COF change and on an edit to a worn wearable; plus a Robust appearance service and agent_appearance_service in the login response.

Enabling it

Region side. These are global: a region section may opt OUT, and is never how you opt in.

[AIS]
    Enabled = true

[Appearance]
    ServerSideBaking = true

A single region opts out with AIS_Enabled = false or ServerSideBaking = false in its own [<Region Name>] section. The per-region AIS key is AIS_Enabled, not Enabled, because a region section holds settings for many modules. Both modules log one INFO line per region at load naming which config decided, for example region Ebony: AIS v3 ON (global).

Robust side. In [ServiceList]:

AppearanceServiceConnector = "${Const|PublicPort}/OpenSim.Server.Handlers.dll:AppearanceServerConnector"

then the service itself:

[AppearanceService]
    LocalServiceModule = "OpenSim.Services.AvatarService.dll:AppearanceService"
    AvatarService = "OpenSim.Services.AvatarService.dll:AvatarService"
    AssetService = "OpenSim.Services.AssetService.dll:AssetService"

and in [LoginService]:

AgentAppearanceServiceURL = "${Const|BaseURL}:${Const|PublicPort}/"

It must reach the connector's port and end with /, because the viewer appends texture/... to it with no separator. Left unset, the login response omits agent_appearance_service entirely and the viewer keeps compositing locally, which is correct for a grid with no such service. The same key exists in config-include/StandaloneCommon.ini.example.

Defaults: [Appearance] BakeSize = 1024 (512, 1024 or 2048) and [Appearance] BakeQuality = 0.85. Changing BakeSize invalidates every stored bake, which is deliberate. [AIS] Enabled and [Appearance] ServerSideBaking both default to false.

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-v3 on 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 of SetUp() and nothing invoked them, so every test relying on a field assigned in setup ran against null. OpenSimTestCase now implements Xunit.IAsyncLifetime and calls SetUp() from InitializeAsync. CoreModules.Tests went from 35 failures to 5. Commit c1fc7fff3e. Proof: Tests/OpenSim.Tests.Common/OpenSimTestCase.cs:43.

  • AgentIsNowWearing wiped 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.Zero still clears, and the appearance is saved only on an actual change. Commit 11a2456833. Proof: AvatarFactoryModule.cs:1360 declaring MergeNowWearing, with AvatarFactoryNowWearingTests.

  • An appearance save wiped the bake index. AvatarService.SetAvatar deletes 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. Commit 1e78b9a706. Proof: Source/OpenSim.Services.Interfaces/IAvatarService.cs:118 declaring AvatarDataKeys, and AvatarService.cs:111 testing AvatarDataKeys.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 in Docs/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. QueueAppearanceSave defers by DelayBeforeAppearanceSave seconds and the handler resolves the ScenePresence only 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. Commit dc4e417bb3. Proof: AvatarFactoryModule.cs:85 subscribing FlushAppearanceSaveOnClose to OnRemovePresence.

  • Duplicate Textures folders, created deterministically. XInventoryService.GetSystemFolders filtered f.type > 0, which excludes type 0 - Textures - so every CreateUserInventory call 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, CreateUserInventory can be entered concurrently for the same principal (it is reachable remotely through XInventoryInConnector) 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. Commits 3057327361 and 68bfa60735. Proof: XInventoryService.cs:329 now filters f.type >= 0 where develop has f.type > 0 at its :202; :116 takes the per-principal lock; :266 is WarnOnDuplicateSystemFolders.

  • ViewerEnvironment.FromWLOSD tested the parameter, not the cast result. A non-array OSD is still non-null, so null reached DayCycle.FromWLOSD(OSDArray) and threw on its first statement. That throw was load-bearing: the legacy WindLight setter called StoreOnRegion immediately 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. Commit f6981e12f9. Proof: Source/OpenSim.Framework/ViewerEnvironment.cs:113-114 and EnvironmentModule.cs:758, with ViewerEnvironmentWLOSDTests.

Wire and interface changes:

  • IInventoryService gains DeleteFolders(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 including XInventoryService, HGInventoryService and HGSuitcaseInventoryService. On the Robust wire the flag is carried as an optional ONLYIFTRASH field 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. Commit e2ad97af70. Proof: Source/OpenSim.Services.Interfaces/IInventoryService.cs:121 and :135; XInventoryInConnector.cs:464 reading ONLYIFTRASH; XInventoryServicesConnector.cs:395 sending 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.md is 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 the ViewerEnvironment fix above.

Tests

project                                  passed  failed  skipped  total
LindenCaps.AIS.Tests                        200       0        0    200
OptionalModules.ServerSideBaking.Tests      115       0        3    118
OpenSimNGC.Appearance.Baking.Tests           31       0        5     36
LindenCaps.Tests                              5       0        0      5
CoreModules.Tests                            76       5        0     81

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.sh generates 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.md is the operator verification script.

Known limitations (all with the flags ON; nothing changes with them off)

  • NPCs: on an SSB region NPCs render correctly in Firestorm but as a cloud in the stock LL viewer, and an owner rebake can delete a bake an NPC clone still references. The fix (bake NPC ids, reference-safe supersede) is planned next; I will push it to this branch if it is still open, or follow up.
  • Viewers log a folder-version "possible version mismatch ... adjusting" line on each wearable save (the viewer expects one more increment than AIS returns). Self-corrects, nothing lost.
  • A region with SSB off collapses a layered outfit to one wearable per type on a COF change made there. Mixed grids should treat the flags as all-or-nothing.
  • POST items-create answers 501 by design: the viewer's own builder for it is compiled out (USE_AIS_FOR_NC is 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.
  • A slam is not transactional across the inventory service (it needs a service-side 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 the CreateUserInventory lock is per Robust process.
  • InventoryFolderBase.Version is ushort against an int(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.
  • The duplicate-system-folder fix (>= 0) has no unit test: Tests/OpenSim.Services.InventoryService.Tests is outside the solution and does not build (NU1605 package downgrades against OpenSim.Tests.Common). Resurrecting it is a follow-up.
  • Do NOT add a unique index on (agentID, type) to inventoryfolders: HG suitcase folders are legitimate duplicates by type, and HGSuitcaseInventoryService.CreateSystemFolders builds a full set of them. A candidate shape is (agentID, parentFolderID, type), not settled and not recommended yet.
  • Bake assets accumulate until expiry; the R,G,B,A,M component order is empirical (the SL bake service source is not public) and verified against reference bakes, not against a citation.
  • On a hypergrid Robust, 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.
  • The AIS system-folder resolution scans the suitcase too. Suitcase COFs sit at version 1 so the root COF always wins today, but nothing enforces that; if a suitcase COF ever overtook the root one, a local user's outfit would resolve into their suitcase.
  • Folder thumbnails and the favorite flag are accepted by AIS and silently dropped, this tree having no column for either. Folder deletion is refused by the hypergrid inventory services.
  • Whether LibreMetaverse exposes RegionProtocols from RegionHandshake to 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

  1. Library placement: OpenSimNGC.Appearance.Baking is 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.

  2. AIS hosting: this is Phase 1, region-side. IAisInventoryBackend is 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 b1da081d1e here; whichever merges second is a no-op.

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
mdickson merged commit 7041278 into OpenSim-NGC:develop Sep 21, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants