Switching 姿 makes her disappear #75

Closed
opened 2026-07-31 13:47:57 +00:00 by aiko · 3 comments
Owner

Reported by the operator on 2026-07-31: "switching models makes them disappear." Observed on the Pixel, against main plus the #62#68 batch.

Not reproduced under instrumentation. updateModelConfig is module-scoped rather than a window global, so the switch cannot be driven from DevTools and I could not capture the console for the failing moment. What follows is the path and the most likely mechanism, not a confirmed root cause — whoever takes this should reproduce it first.

The path

use-live2d-model.ts's model effect fires when modelInfo.url changes. It calls updateModelConfig(...) and then, after 500ms:

LAppLive2DManager.releaseInstance();
initializeLive2D();

initializeLive2D (WebSDK/src/main.ts) releases the manager again if a delegate exists, then guards on:

if (!LAppGlManager.getInstance() || !LAppDelegate.getInstance().initialize()) {
  console.error("Failed to initialize Live2D");
  return;
}

Why this is the same family as #68

#68 was the same singletons losing their canvas. It was fixed by never unmounting the canvas — the model is now portalled into a host that survives both the mode branch and the onboarding gate.

What #68 did not touch is the release-and-reinitialise path, which is the only path that still tears Cubism down deliberately. LAppGlManager.getInstance() returns the existing singleton bound to the original #canvas element, and only LAppLive2DManager is released — the delegate, the GL manager and the view are reused. If initialize() returns false, or returns true against a stale view, the manager reloads the new model into something that no longer draws.

The comment left on that timer in #68's wake says the cleanup stays because "the model url can change under a 姿 switch". That is exactly the case being reported, and it is the one case that was never exercised.

What to check first, in order

  1. Reproduce and read the console. works.aiko.ollvt.debug is a debug build with webview debugging on: adb forward tcp:9222 localabstract:webview_devtools_remote_<pid> and watch for Failed to initialize Live2D at the moment of the switch. That one line splits this in two: guard failure versus a silent reuse of a dead view.
  2. Whether the new model loads at all. [CSM][E]Failed to load file in logcat means it is an asset problem rather than a GL one — note #69's sibling, where the model url was resolved against the wrong origin.
  3. Whether the 500ms timer still makes sense. It exists to lose a race that #68 removed for mode switches. If a switch now only ever happens from one place, the delay may be delaying the very re-init the operator is waiting for.

Also worth knowing

#52 made 姿 offer the go-between's real model register, so switching is now a normal thing to do rather than a developer's edge case. model_dict.json currently holds mao_pro and xiaozhanglang, so this is reachable in two taps.

Acceptance

  • Switching 姿 leaves her drawn, on desktop and on Android
  • Switching back and forth repeatedly does not degrade — the singletons survive N switches, not just one
  • Whatever the mechanism turns out to be, it is written down beside the code that survives it, the way #68's host is
  • A test pins it as far as a suite with no jsdom can — at minimum that the release-and-reinitialise path is entered exactly once per switch
  • Verified by switching on a real device, not by reading the path

denpa#68, denpa#52, denpa#69

Reported by the operator on 2026-07-31: *"switching models makes them disappear."* Observed on the Pixel, against `main` plus the `#62`–`#68` batch. **Not reproduced under instrumentation.** `updateModelConfig` is module-scoped rather than a window global, so the switch cannot be driven from DevTools and I could not capture the console for the failing moment. What follows is the path and the most likely mechanism, not a confirmed root cause — whoever takes this should reproduce it first. ## The path `use-live2d-model.ts`'s model effect fires when `modelInfo.url` changes. It calls `updateModelConfig(...)` and then, after 500ms: ``` LAppLive2DManager.releaseInstance(); initializeLive2D(); ``` `initializeLive2D` (`WebSDK/src/main.ts`) releases the manager again if a delegate exists, then guards on: ``` if (!LAppGlManager.getInstance() || !LAppDelegate.getInstance().initialize()) { console.error("Failed to initialize Live2D"); return; } ``` ## Why this is the same family as `#68` `#68` was the same singletons losing their canvas. It was fixed by never unmounting the canvas — the model is now portalled into a host that survives both the mode branch and the onboarding gate. **What `#68` did not touch is the release-and-reinitialise path**, which is the only path that still tears Cubism down deliberately. `LAppGlManager.getInstance()` returns the *existing* singleton bound to the original `#canvas` element, and only `LAppLive2DManager` is released — the delegate, the GL manager and the view are reused. If `initialize()` returns false, or returns true against a stale view, the manager reloads the new model into something that no longer draws. The comment left on that timer in `#68`'s wake says the cleanup stays because "the model url can change under a 姿 switch". That is exactly the case being reported, and it is the one case that was never exercised. ## What to check first, in order 1. **Reproduce and read the console.** `works.aiko.ollvt.debug` is a debug build with webview debugging on: `adb forward tcp:9222 localabstract:webview_devtools_remote_<pid>` and watch for `Failed to initialize Live2D` at the moment of the switch. That one line splits this in two: guard failure versus a silent reuse of a dead view. 2. **Whether the new model loads at all.** `[CSM][E]Failed to load file` in logcat means it is an asset problem rather than a GL one — note `#69`'s sibling, where the model url was resolved against the wrong origin. 3. **Whether the 500ms timer still makes sense.** It exists to lose a race that `#68` removed for mode switches. If a switch now only ever happens from one place, the delay may be delaying the very re-init the operator is waiting for. ## Also worth knowing `#52` made 姿 offer the go-between's real model register, so switching is now a normal thing to do rather than a developer's edge case. `model_dict.json` currently holds `mao_pro` and `xiaozhanglang`, so this is reachable in two taps. ## Acceptance - [ ] Switching 姿 leaves her drawn, on desktop and on Android - [ ] Switching back and forth repeatedly does not degrade — the singletons survive N switches, not just one - [ ] Whatever the mechanism turns out to be, it is written down beside the code that survives it, the way `#68`'s host is - [ ] A test pins it as far as a suite with no jsdom can — at minimum that the release-and-reinitialise path is entered exactly once per switch - [ ] Verified by switching on a real device, not by reading the path ## Related `denpa#68`, `denpa#52`, `denpa#69`
Author
Owner

This was generated by AI during triage.

Not reproducible on the desktop from a clean start, on main with #69 and #76 merged. The operator switched 姿 repeatedly, both directions, and switched window → pet → window before switching again. She stayed drawn every time. Leaving this open rather than closing it: the original report was on the Pixel, against a built APK, and that has not been retested.

What the instrumentation established

The issue's premise is wrong in one specific way. It says the release-and-reinitialise path 'is the one case that was never exercised'. It runs on every switch — probes at four points around it (before release, after init, +1s, +3s) fired on each one, and the model loaded correctly each time: updateModelConfig received the right resourcePath, modelDirectory and modelFileName, textures were set up, and the Cubism core reported its version. No Failed to initialize Live2D, no [CSM][E]Failed to load file. So check 1 and check 2 in 'What to check first' are both answered, negatively.

What was found instead

A deterministic trigger for the same mechanism, in dev. Any Fast Refresh of components/canvas/live2d.tsx — a single hmr update line, no page reload — loses her permanently, exactly as described here. Fast Refresh remounts the component, which recreates <canvas id="canvas">. LAppGlManager binds document.getElementById('canvas') once, into a module-level singleton, and nothing anywhere calls LAppGlManager.releaseInstance(), so Cubism keeps drawing into a canvas React has already discarded. The window is decorationless and transparent, so the result is an invisible window — the same symptom #68 opens with.

This matters beyond dev: it means #68's fix is narrower than it reads. The portal stops a mode switch from remounting her; it does not stop the class. Any remount of Live2D from any cause has this outcome, and the GL manager still has no rebind path.

It also means several 'reproductions' earlier today were mine, not the product's. Editing files under a running dev server produced the symptom repeatedly and confounded the first two attempts at this. Anything observed under HMR should be discarded.

What is left

  • Retest on the Pixel, on a built APK, which is where this was first seen. That is the open question.
  • The rebind gap is real regardless. Whether it is worth fixing here or as its own ticket depends on whether a production path still remounts Live2D; #77 (she is not drawn in pet mode) may be the same root and is filed separately.

Also observed, not this issue

mic already open on every reconnect — vad-context.tsx:512 logs it and :524 leaves an unhandled promise rejection. It fired on every reconnect during testing. Not filed yet; say the word and it gets a ticket.

> *This was generated by AI during triage.* **Not reproducible on the desktop from a clean start, on `main` with `#69` and `#76` merged.** The operator switched 姿 repeatedly, both directions, and switched window → pet → window before switching again. She stayed drawn every time. Leaving this open rather than closing it: the original report was on the **Pixel**, against a built APK, and that has not been retested. ## What the instrumentation established The issue's premise is wrong in one specific way. It says the release-and-reinitialise path *'is the one case that was never exercised'*. It runs on every switch — probes at four points around it (`before release`, `after init`, `+1s`, `+3s`) fired on each one, and the model loaded correctly each time: `updateModelConfig` received the right `resourcePath`, `modelDirectory` and `modelFileName`, textures were set up, and the Cubism core reported its version. No `Failed to initialize Live2D`, no `[CSM][E]Failed to load file`. So check 1 and check 2 in *'What to check first'* are both answered, negatively. ## What was found instead **A deterministic trigger for the same mechanism, in dev.** Any Fast Refresh of `components/canvas/live2d.tsx` — a single `hmr update` line, no page reload — loses her permanently, exactly as described here. Fast Refresh remounts the component, which recreates `<canvas id="canvas">`. `LAppGlManager` binds `document.getElementById('canvas')` **once**, into a module-level singleton, and nothing anywhere calls `LAppGlManager.releaseInstance()`, so Cubism keeps drawing into a canvas React has already discarded. The window is decorationless and transparent, so the result is an invisible window — the same symptom `#68` opens with. This matters beyond dev: it means `#68`'s fix is narrower than it reads. The portal stops a **mode switch** from remounting her; it does not stop the class. Any remount of `Live2D` from any cause has this outcome, and the GL manager still has no rebind path. It also means several 'reproductions' earlier today were mine, not the product's. Editing files under a running dev server produced the symptom repeatedly and confounded the first two attempts at this. Anything observed under HMR should be discarded. ## What is left - **Retest on the Pixel**, on a built APK, which is where this was first seen. That is the open question. - **The rebind gap is real regardless.** Whether it is worth fixing here or as its own ticket depends on whether a production path still remounts `Live2D`; `#77` (she is not drawn in pet mode) may be the same root and is filed separately. ## Also observed, not this issue `mic already open` on every reconnect — `vad-context.tsx:512` logs it and `:524` leaves an unhandled promise rejection. It fired on every reconnect during testing. Not filed yet; say the word and it gets a ticket.
Author
Owner

This was generated by AI during triage.

Moved back to ready-for-human. The original report was on the Pixel, and the desktop pass on 2026-07-31 could not reproduce it; the acceptance says plainly "verified by switching on a real device, not by reading the path". There is no failing test to derive from here until someone watches it fail again — the next step is a switch on the phone with adb forward tcp:9222 localabstract:webview_devtools_remote_<pid> open, looking for Failed to initialize Live2D. An agent cannot take that step.

> *This was generated by AI during triage.* Moved back to `ready-for-human`. The original report was on the Pixel, and the desktop pass on 2026-07-31 could not reproduce it; the acceptance says plainly *"verified by switching on a real device, not by reading the path"*. There is no failing test to derive from here until someone watches it fail again — the next step is a switch on the phone with `adb forward tcp:9222 localabstract:webview_devtools_remote_<pid>` open, looking for `Failed to initialize Live2D`. An agent cannot take that step.
Author
Owner

Closed. The canvas run fixed it: f334feb stops one mistimed attempt deciding the whole session, a7a103f says whether she is coming or not coming, acb29b5 stops watching an absent model at the pace of hope, with aac27ae applying the default expression to the model being switched to. Verified by the operator.

Closed. The canvas run fixed it: `f334feb` stops one mistimed attempt deciding the whole session, `a7a103f` says whether she is coming or not coming, `acb29b5` stops watching an absent model at the pace of hope, with `aac27ae` applying the default expression to the model being switched to. Verified by the operator.
aiko closed this issue 2026-08-10 22:51:47 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiko/denpa#75
No description provided.