netplay: per-player own-view mods, widescreen only inside the local view

A [[plugin]] may declare netplay = "local_view": it only changes what one
player sees. A netplay session now keeps the player's own features whose every
contribution is such a plugin (no writes, overlays or derived discs) instead of
clearing them, without saving state.toml. They activate as usual, but their
hooks run only inside psx_mod_render_local_view (psx_mod_local_view_scope) and
their vblank/savestate callbacks not at all. With such a plan the widescreen
cull margin is the stock 0 in the shared game and this peer's own margin only
inside the sandboxed own-view draw; squash is not used. The canonical frame,
snapshots and digests stay identical on every peer whatever each player chose,
so nothing is negotiated, and each keeps their own aspect.

Tests: netplay_view_mods_test (flag, plan filter, parked hooks, no vblank,
state.toml untouched), ws_local_view_margin_test (margin scope), render-pass
sandbox asserts the scope around a local view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Shokunin
2026-10-06 14:17:26 -07:00
parent f379771c93
commit 33b842e97f
19 changed files with 561 additions and 23 deletions
+39 -4
View File
@@ -139,7 +139,8 @@ Internal resolution is not a mod, so it is not cleared for netplay: each peer
keeps its own preset, clamped to its own GPU, and one peer at 4K next to another
at Native is a supported match. Only the GL present surface changes; the 1×
software authority, the digests and the rollback snapshots are the same on every
peer. Widescreen and frame-rate mods, by contrast, are cleared for netplay.
peer. Mods are cleared for netplay, except a player's own-view mods (below),
which stay per player and never touch the shared game.
### Each peer's own view (presentation only)
@@ -329,9 +330,43 @@ Generate & rebuild / prepare flows should point at the **`.cue`**, not a lone
netplay.
- **VERSION / lobby match pin:** peers should run the same release pin so
generated code and protocol stay compatible.
- **Mods:** disabled for all netplay sessions (lobby / LAN / direct / rematch).
Launcher `commit_netplay` and the runtime clear the in-session plan without
touching the user's offline mod selection. Synced mod plans are deferred.
- **Mods:** cleared for all netplay sessions (lobby / LAN / direct / rematch)
without touching the user's offline mod selection, except own-view mods.
### Own-view mods (per player)
The shared simulation stays stock on every peer; a mod that only changes
what one player sees runs for that player alone, inside the sandboxed own-view
render (`psx_mod_render_local_view`). A package opts in per plugin:
```toml
[[plugin]]
feature = "widescreen"
id = "r4.widescreen"
netplay = "local_view"
```
At a netplay session start the runtime resolves the player's own selection
and keeps only features whose every contribution is such a plugin (no EXE or
disc write, overlay or derived disc; `mod_runtime_commit_netplay_view`). They
activate as usual (presentation state: aspect, scene predicates), but:
- their function-entry, filter, guest-function and instruction hooks run only
while `psx_mod_local_view_scope()` is 1, i.e. inside a local-view draw, whose
guest-side effects the sandbox discards; vblank and savestate callbacks do
not run in a match;
- `gpu_ws_set_local_view_only(1)`: the widescreen cull margin
(`psx_ws_x_margin()`) is the stock 0 in the shared game and this peer's own
margin only inside the local-view draw; squash widescreen (which changes the
GTE projection) is not used.
So the canonical frame, rollback snapshots and digests are identical on every
peer whatever each player chose, there is nothing to negotiate or hash, and
each player keeps their own aspect (Fit follows their own window). Nothing is
written to `mods/state.toml`. A title without an own view simply shows its
canonical frame. A plugin must not keep host state that feeds guest writes
outside the scope; anything that persists guest state across frames is not an
own-view mod.
---