Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions .github/workflows/live-shutdown-windows.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Windows live-session shutdown

on:
pull_request:
paths:
- .github/workflows/live-shutdown-windows.yml
- CMakeLists.txt
- cpp/**
- python/**
- pyproject.toml
- uv.lock
push:
branches: [master, marpaia/17]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: windows-live-shutdown-${{ github.ref }}
cancel-in-progress: true

jobs:
shutdown:
runs-on: windows-2025
timeout-minutes: 20
defaults:
run:
shell: pwsh
env:
CMAKE_BUILD_PARALLEL_LEVEL: "2"
# Exercise the host reference engine without requiring a GPU toolkit.
CMAKE_ARGS: -DCM_ENABLE_METAL=OFF -DCM_ENABLE_CUDA=OFF -DCM_BUILD_TESTS=OFF
steps:
- name: Check out source
uses: actions/checkout@v6
with:
persist-credentials: false

- name: Set up uv
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true
python-version: "3.12"

- name: Build CPU extension and install locked test dependencies
run: uv sync --locked --group dev

- name: Record platform, shell, interpreter, and backend
run: |
New-Item -ItemType Directory -Force build/shutdown-evidence | Out-Null
$PSVersionTable | Out-File build/shutdown-evidence/platform.txt
[System.Environment]::OSVersion | Out-File -Append build/shutdown-evidence/platform.txt
uv run --no-sync python --version 2>&1 | Tee-Object -Append build/shutdown-evidence/platform.txt
uv run --no-sync microsimulator devices --json | Tee-Object -Append build/shutdown-evidence/platform.txt

- name: Verify Stop, console Ctrl+C, worker draining, and same-port restart
run: >-
uv run --no-sync python -m pytest
python/tests/test_viewer_server.py python/tests/test_viewer_shutdown.py
-v --tb=short --junitxml=build/shutdown-evidence/tests.xml

- name: Upload shutdown evidence
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: windows-live-shutdown-${{ github.sha }}
path: build/shutdown-evidence
if-no-files-found: warn
retention-days: 30
Binary file added docs/assets/capsules/colony-after.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/capsules/colony-before.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/capsules/isolated-after.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/capsules/isolated-before.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 40 additions & 0 deletions docs/capsule-rendering-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Capsule rendering validation

The previous renderer (`b69193b9`) combined a closed cylinder with two complete spheres. The sphere tessellation remained in world orientation while the cylinder followed the cell direction. Their polygonal boundaries did not coincide, and the cylinder end disks intersected the spherical surfaces. The resulting narrow lines at the cap joins are visible in the baseline screenshots below.

The replacement uses an open cylinder and two hemispheres, all sharing the cell orientation. Both hemispherical equators use the cylinder's exact radial samples and normals. Radius scales the caps uniformly; cylindrical length scales only the cylinder and displaces cap centers. The selection wireframe uses these same surfaces with an 8% radius expansion. Zero centerline length collapses the cylinder and joins two hemispheres into a sphere.

## Identical-camera comparisons

These are unmodified screenshots from `viewer/browser/capsules.mjs`, using the same geometry fixtures, camera, lights, colors, browser, and viewport. The baseline renderer was recorded before the implementation changed. Neither image comes from a different simulation trajectory.

| Fixture | Before | After |
| --- | --- | --- |
| Isolated rod, arbitrary 3D direction | ![Baseline isolated rod with a visible cap join](assets/capsules/isolated-before.png) | ![Continuous isolated capsule](assets/capsules/isolated-after.png) |
| Dense colony, near view | ![Baseline cap rings throughout the colony](assets/capsules/colony-before.png) | ![Colony without the cap rings](assets/capsules/colony-after.png) |

The cap-join lines disappear at the same camera positions in the near and distant fixtures. The browser harness also records 48 frames of prescribed changes in position and orientation, so the surface can be inspected during movement. Silhouette tessellation, pixel aliasing, and real motion remain possible; this change does not smooth simulation state or claim to eliminate every source of shimmer.

## Geometry and interaction checks

The Vitest geometry suite checks matched seam positions/normals, outward-facing hemisphere triangles, absence of cylinder end disks, constant distance from the centerline segment, exact axial extents, conservative bounds, arbitrary directions, and zero-length cells. It also ray-picks both capsule tips with real Three.js instanced meshes and checks selection-radius inflation. The browser harness exercises color attributes on every mesh, pointer picking, stable-ID selection through reordered frames and removal, actual selected-overlay surface positions, and a selected zero-length sphere. The largest measured overlay-surface error in the browser fixture was `1.29e-8` world units.

## Local rendering and resource measurements

Both comparisons ran on macOS in headless Chromium `153.0.8010.12`, with the same browser launch configuration. The corrected run reports ANGLE/Vulkan SwiftShader: these are **software WebGL measurements**, not physical GPU performance results. The fixture uses 512 cells, ten warmup frames, and 60 measured complete frame replacements, including transforms, coloring, rendering, and `gl.finish()` synchronization. Small timing differences are within local measurement noise.

| Measurement | Before | After |
| --- | ---: | ---: |
| Instanced draw calls for the colony | 3 | 3 |
| Shared geometry vertices (cylinder + both caps) | 388 | 400 |
| Triangles per cell | 504 | 576 |
| Triangles for 512 cells | 258,048 | 294,912 |
| Median replacement/render time | 2.2 ms | 2.1 ms |
| p95 replacement/render time | 2.4 ms | 2.4 ms |
| Renderer geometry count at both sampled frames | 4 | 4 |
| Tracked live WebGL buffers across 59 replacements | 390 → 744 | 18 → 18 |
| Tracked buffers after an empty frame | 732 | 0 |

The geometry count alone concealed an existing resource leak: removing an instanced mesh and disposing its geometry did not release `instanceMatrix` and `instanceColor` buffers. Frame replacement now calls `InstancedMesh.dispose()` as well as disposing each shared geometry/material once. Browser instrumentation observes actual WebGL buffer creation/deletion; buffers remain bounded across replacements and return to zero after clearing the colony and disposing the viewer.

The modest tessellation change (24 radial samples and six rows per hemisphere) improves silhouettes, but it is not the basis for the seam fix: the tests verify the changed surface topology and exact equator agreement. For reproduction commands, fixture details, videos, and resource assertions, see [the browser harness instructions](../viewer/browser/README.md).
20 changes: 20 additions & 0 deletions docs/protocols/live-viewer-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ Rejected commands and model failures return a data-only error:
{ "type": "error", "message": "reason" }
```

Intentional shutdown sends lifecycle notifications before closing the WebSocket with code 1000:

```json
{"type":"session","state":"stopping"}
{"type":"session","state":"stopped"}
```

`stopping` means admission of new work has ended. `stopped` means the active operation has finished and the simulation worker has terminated. Clients should process previously received messages (including asynchronous scene verification) before interpreting the subsequent socket close. A close without `stopped` is still an unexpected disconnect. These messages extend the v1 vocabulary; use a viewer built from the same release as the server.

## Client commands

The vocabulary is closed. Unknown fields are rejected.
Expand All @@ -48,8 +57,19 @@ The vocabulary is closed. Unknown fields are rejected.
{"type":"pause"}
{"type":"reset"}
{"type":"checkpoint"}
{"type":"stop"}
```

`steps` defaults to one and is bounded to 1 through 10,000. Playback advances the configured number of steps per published frame. A step or reset first pauses playback. Disconnecting the final client pauses the simulation.

Reset calls the original server-side model factory again with its original backend, device, seed, parameters, and resume source. Checkpoint writes only to the destination configured when the server starts and atomically replaces that file. It preserves controller state for any runnable model implementing the `SimulationController` protocol, including the legacy compatibility adapter.

## Stop and restart

Stop ends this server process and releases its listening port. It is authenticated through the same token and exact-origin WebSocket upgrade as every other command. The reader handles Stop immediately, including while an earlier command on that same socket is executing. Ordinary commands retain per-client ordering in a bounded queue of 32; additional queued commands receive an error rather than blocking Stop.

Shutdown is cooperative: an in-progress individual simulation step finishes, then the rest of its batch is skipped. An already-running reset, scene capture, or atomic checkpoint write also finishes. There is no timeout that kills the worker in the middle of model state mutation or file replacement. A model operation that never returns will therefore keep the session in `stopping`. Queued operations and newly received Frame, Play, Step, Pause, Reset, and Checkpoint commands are rejected once stopping begins; repeated Stop requests are idempotent. Closing the final browser connection still pauses the session and allows reconnection.

Browser Stop and terminal Ctrl+C share the same worker/socket cleanup path. After `stopped`, the server closes client sockets and its application runner, exits, and releases the port. Launch the next `microsimulator view` command from the terminal and open its newly printed URL; each process has a new token. The old browser keeps its last rendered frame and displays Stopped.

Network delivery has separate deadlines from cooperative model work. Initial frame writes, broadcasts, and lifecycle notifications allow one second per receiver; independent lifecycle deliveries run concurrently. The complete WebSocket close operation, including writing and draining its close frame, also has a one-second deadline. An unresponsive connection is then aborted, discarding queued network bytes so neither its initial-send handler nor the application runner waits indefinitely for a reader. Responsive clients still receive `stopping`, `stopped`, and a normal code-1000 close. A stalled client may miss these notifications and observe an unexpected disconnect; this does not cancel an active simulation operation or interrupt an atomic checkpoint write.
Loading