She stops rendering after any mode switch, and nothing but a restart brings her back #68

Closed
opened 2026-07-31 09:01:14 +00:00 by aiko · 1 comment
Owner

Found on 2026-07-31 by switching modes on the desktop. Two failed fix attempts are recorded below so nobody repeats them.

Reproduction

  1. Start the app. She renders in window mode.
  2. Switch to pet mode — she is not drawn. Pet mode is a transparent, undecorated, mouse-passthrough window whose only visible content is the model, so the result is an invisible window: nothing to see and nothing to click.
  3. Switch back to window mode — she is still not drawn. Nothing short of restarting the app brings her back.

Confirmed by the operator across several restarts. Cold start always works; the first mode switch always loses her permanently.

The tray does not rescue it. The tray menu has window/pet items and Show/Hide, and none of them restored the model or produced a visible window. So a lost model in pet mode currently requires killing the process — arguably worse than the render bug, because the tray is supposed to be the way back from an invisible window.

What is established

<Live2D /> is mounted at three different places in the tree, one per mode: App.tsx:44 (inside WindowReceiverReceiverChrome), :160 (Android, inside OverlayPalContainer), :165 (desktop pet, inside a positioned Box). React therefore unmounts one canvas and mounts another on every mode change.

The Cubism layer underneath is all module singletons bound to a single global DOM id:

  • WebSDK/src/lappglmanager.ts:42 — the constructor does canvas = document.getElementById('canvas') once, into a module-level s_instance.
  • WebSDK/src/main.ts:26-29initializeLive2D releases only LAppLive2DManager. Neither LAppDelegate nor LAppGlManager is released.
  • WebSDK/src/main.ts:31-33 — the guard is !LAppGlManager.getInstance() || !LAppDelegate.getInstance().initialize(). A surviving-but-stale GL manager satisfies it, so initialisation reports success.
  • Nothing anywhere else calls releaseInstance: grep across src/renderer/src returns exactly one hit, inside that one effect (hooks/canvas/use-live2d-model.ts:131).

So the leading hypothesis remains: after a mode switch the GL manager still holds the WebGL context of a canvas React has already removed, init "succeeds" against it, and nothing is drawn. It is a hypothesis, not a finding — see below.

Two fixes that did not work

1. Clearing the re-init timer on unmount. use-live2d-model.ts:129 scheduled release-and-reinit on a 500ms setTimeout with no cleanup, while the effect directly below it at :175-182 clears its own timer. The theory was that the departing mode's timer fired after the arriving canvas mounted and tore it down. Tested by the operator: she still disappears. The cleanup was kept anyway — an uncleaned timer is a real defect and the sibling effect proves the house style — but it fixes nothing observed. Shipped in 985809c.

2. Releasing the delegate and the GL manager too, so the constructor would re-bind to the live canvas:

LAppLive2DManager.releaseInstance();
LAppDelegate.releaseInstance();
LAppGlManager.releaseInstance();

This broke cold start. She stopped rendering at all, not just after a switch. Reverted; main.ts is untouched on main. Whatever initializeLive2D rebuilds afterwards, it does not rebuild everything those two releases destroy. Do not simply retry this.

What to try instead

Stop unmounting the canvas. Mount <Live2D /> once, above the mode branch, and move it between the chrome's stage and the pal's slot with CSS rather than with the React tree. That deletes the whole class of bug instead of making teardown work, and it is the only approach that does not depend on the Cubism singletons being safely re-creatable — which attempt 2 shows they are not.

App.tsx:108-110 already hints this was the intent:

Pet mode is the transparent desktop pal (design 1b): she is most of the window, bottom-left … The canvas lives outside PalContainer's subtree, so it takes the group's scale from the CSS variable the pal publishes.

The awkward part is window mode, where the canvas is a child of ReceiverChrome's stage band and takes its geometry from that layout. Options: a stable fixed-position canvas whose rect tracks the stage band, or a portal into a slot the chrome renders. This wants a considered run, not a patch at the end of a session.

Notes for whoever takes it

  • The renderer console is the missing instrument. Every finding above is read off the source; nothing here was observed at runtime, because the only place the renderer console goes is WebView2's devtools and screenshots of that window come out pure black. Getting one console log out of a mode switch is worth more than another hour of reading.
  • The test suite is no help: 810 vitest tests pass through every state described here. Nothing in it touches WebGL.
  • Whether the Android overlay path (:160) is affected by the same cause is unverified. The Pixel shows an invisible avatar too, but it is running an APK from 2026-07-30 23:30 and has not been retested since.

denpa#35, denpa#67, design § 1b / § 2a

Found on 2026-07-31 by switching modes on the desktop. **Two failed fix attempts are recorded below so nobody repeats them.** ## Reproduction 1. Start the app. She renders in window mode. 2. Switch to pet mode — **she is not drawn.** Pet mode is a transparent, undecorated, mouse-passthrough window whose only visible content is the model, so the result is an invisible window: nothing to see and nothing to click. 3. Switch back to window mode — **she is still not drawn.** Nothing short of restarting the app brings her back. Confirmed by the operator across several restarts. Cold start always works; the first mode switch always loses her permanently. **The tray does not rescue it.** The tray menu has window/pet items and Show/Hide, and none of them restored the model or produced a visible window. So a lost model in pet mode currently requires killing the process — arguably worse than the render bug, because the tray is supposed to be the way back from an invisible window. ## What is established `<Live2D />` is mounted at **three different places in the tree**, one per mode: `App.tsx:44` (inside `WindowReceiver` → `ReceiverChrome`), `:160` (Android, inside `OverlayPalContainer`), `:165` (desktop pet, inside a positioned `Box`). React therefore unmounts one canvas and mounts another on every mode change. The Cubism layer underneath is all module singletons bound to a single global DOM id: - `WebSDK/src/lappglmanager.ts:42` — the constructor does `canvas = document.getElementById('canvas')` **once**, into a module-level `s_instance`. - `WebSDK/src/main.ts:26-29` — `initializeLive2D` releases **only** `LAppLive2DManager`. Neither `LAppDelegate` nor `LAppGlManager` is released. - `WebSDK/src/main.ts:31-33` — the guard is `!LAppGlManager.getInstance() || !LAppDelegate.getInstance().initialize()`. A surviving-but-stale GL manager satisfies it, so initialisation reports success. - Nothing anywhere else calls `releaseInstance`: grep across `src/renderer/src` returns exactly one hit, inside that one effect (`hooks/canvas/use-live2d-model.ts:131`). So the leading hypothesis remains: after a mode switch the GL manager still holds the WebGL context of a canvas React has already removed, init "succeeds" against it, and nothing is drawn. **It is a hypothesis, not a finding** — see below. ## Two fixes that did not work **1. Clearing the re-init timer on unmount.** `use-live2d-model.ts:129` scheduled release-and-reinit on a 500ms `setTimeout` with no cleanup, while the effect directly below it at `:175-182` clears its own timer. The theory was that the departing mode's timer fired after the arriving canvas mounted and tore it down. **Tested by the operator: she still disappears.** The cleanup was kept anyway — an uncleaned timer is a real defect and the sibling effect proves the house style — but it fixes nothing observed. Shipped in `985809c`. **2. Releasing the delegate and the GL manager too**, so the constructor would re-bind to the live canvas: ```ts LAppLive2DManager.releaseInstance(); LAppDelegate.releaseInstance(); LAppGlManager.releaseInstance(); ``` **This broke cold start.** She stopped rendering at all, not just after a switch. Reverted; `main.ts` is untouched on `main`. Whatever `initializeLive2D` rebuilds afterwards, it does not rebuild everything those two releases destroy. **Do not simply retry this.** ## What to try instead **Stop unmounting the canvas.** Mount `<Live2D />` once, above the mode branch, and move it between the chrome's stage and the pal's slot with CSS rather than with the React tree. That deletes the whole class of bug instead of making teardown work, and it is the only approach that does not depend on the Cubism singletons being safely re-creatable — which attempt 2 shows they are not. `App.tsx:108-110` already hints this was the intent: > Pet mode is the transparent desktop pal (design 1b): she is most of the window, bottom-left … **The canvas lives outside PalContainer's subtree**, so it takes the group's scale from the CSS variable the pal publishes. The awkward part is window mode, where the canvas is a child of `ReceiverChrome`'s stage band and takes its geometry from that layout. Options: a stable fixed-position canvas whose rect tracks the stage band, or a portal into a slot the chrome renders. **This wants a considered run, not a patch at the end of a session.** ## Notes for whoever takes it - **The renderer console is the missing instrument.** Every finding above is read off the source; nothing here was observed at runtime, because the only place the renderer console goes is WebView2's devtools and screenshots of that window come out pure black. Getting one console log out of a mode switch is worth more than another hour of reading. - The test suite is no help: 810 vitest tests pass through every state described here. Nothing in it touches WebGL. - Whether the Android overlay path (`:160`) is affected by the same cause is unverified. The Pixel shows an invisible avatar too, but it is running an APK from 2026-07-30 23:30 and has not been retested since. ## Related `denpa#35`, `denpa#67`, design § 1b / § 2a
Author
Owner

This was generated by AI during triage.

Merged to main in ee25aaa (Merge the #62-#68 batch).

Fix. App.tsx now portals the model into a live2dHost that survives both the mode branch and the onboarding gate, so React no longer unmounts one canvas and mounts another on a mode change. The Cubism singletons keep the canvas they were bound to instead of being re-initialised against a removed one.

The uncleaned 500ms timer in use-live2d-model.ts was also given its cleanup — that was a real defect even though, as recorded on this issue, it was not the cause.

Tests. live2d-one-canvas.test.ts pins that there is exactly one canvas across mode switches. Gate on merged main: Test Files 62 passed (62) / Tests 854 passed (854).

Still open, deliberately. The release-and-reinitialise path this fix did not touch is #75 — switching 姿 still loses her, reproduced by the operator on 2026-07-31. Same singletons, different trigger.

> *This was generated by AI during triage.* Merged to `main` in `ee25aaa` (Merge the #62-#68 batch). **Fix.** `App.tsx` now portals the model into a `live2dHost` that survives both the mode branch and the onboarding gate, so React no longer unmounts one canvas and mounts another on a mode change. The Cubism singletons keep the canvas they were bound to instead of being re-initialised against a removed one. The uncleaned 500ms timer in `use-live2d-model.ts` was also given its cleanup — that was a real defect even though, as recorded on this issue, it was not the cause. **Tests.** `live2d-one-canvas.test.ts` pins that there is exactly one canvas across mode switches. Gate on merged main: `Test Files 62 passed (62) / Tests 854 passed (854)`. **Still open, deliberately.** The release-and-reinitialise path this fix did not touch is [#75](https://git.aiko.works/aiko/denpa/issues/75) — switching 姿 still loses her, reproduced by the operator on 2026-07-31. Same singletons, different trigger.
aiko closed this issue 2026-07-31 14:33:21 +00:00
aiko referenced this issue from a commit 2026-08-07 08:57:15 +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#68
No description provided.