Move 36 platform-specific files into per-module `windows/` and `linux/` subfolders (and the
shared HID codecs into `inject/proto/`):
capture/{windows,linux}/ encode/{windows,linux}/ inject/{windows,linux,proto}/
audio/{windows,linux}/ vdisplay/{windows,linux}/
src/windows/ (service, wgc_helper, win_adapter, win_display)
src/linux/ (dmabuf_fence, drm_sync, zerocopy/)
Done with `#[path]`, NOT a module rename: every file moves into its folder while the
`crate::*::*` module names stay FLAT, so all caller paths and every internal `super::`/`crate::`
reference are unchanged — only the parent `mod` decls gained `#[path = "..."]`. This is the
codebase's existing pattern (inject's gamepad_windows) and makes the move byte-identical in
behaviour with ZERO reference churn, far lower risk than collapsing to a single
`crate::capture::windows::` namespace (that deeper rename is an optional follow-on; this delivers
the cfg-sprawl folder confinement the stage is about). Done LAST, after the semantic stages, so
the path churn didn't fight them.
Verified: Linux cargo check + clippy (-D warnings) clean; my mod-decl changes fmt-clean (the 3
remaining fmt diffs are pre-existing local-rustfmt-version skew that moved with their files); all
36 `#[path]` targets exist; no internal `#[path]`/`include!`/file-child-mod in any moved file
(the inline `mod X {` blocks are self-contained). Box build to follow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The headline §2.3 seam tightening (the explicit Stage-3 deferral; §5's "highest-severity
coupling"): the capturer is now TOLD its output format instead of re-deriving the encode backend.
New `capture::OutputFormat { gpu, hdr }`, resolved once per session and passed INTO
capture_virtual_output:
* native punktfunk/1 path: `SessionPlan::output_format()` (gpu = encoder.is_gpu(), from the
already-resolved plan.encoder — no second probe; hdr = plan.hdr).
* GameStream + spike paths: `OutputFormat::resolve(hdr)` (gpu from the single `gpu_encode()`
source, which maps windows_resolved_backend()).
`capture/dxgi.rs DuplCapturer::open` takes `gpu` in and its internal
`!matches!(windows_resolved_backend(), Software)` recompute is DELETED — the capture layer no
longer re-calls the encode layer (the back-reference that could let capture and encode disagree
on whether frames are GPU-resident, plan §2.3/§5). The relay's secure-desktop DDA passes
`gpu_encode()` likewise.
Behavior-preserving: the `gpu` passed in equals the value the capturer used to compute (same
encode-backend resolution). The DDA opens keep `want_hdr=false` (the SDR fallback, unchanged).
Tightenings 2 (HDR/release -> VirtualLease) and 3 (EncoderCaps) split off: (2) needs the
monitor-generation carried on the lease + the keepalive becoming Box<dyn VirtualLease> — that's
the §2.5 ownership-model change (CURRENT_MON_GEN / sudovda::wait_for_monitor_released), so it
moves there; (3) is a small additive follow-on. Documented in the plan.
Verified: Linux cargo check + clippy (-D warnings) + fmt clean. Box build to follow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bundle the 13-positional-argument `#[allow(too_many_arguments)]` session entry (virtual_stream
AND virtual_stream_relay) into one owned SessionContext struct, moved into the stream thread.
The reconfig/keyframe receivers move IN (virtual_stream is their only consumer), retiring the
&Receiver borrow plumbing. Behavior-identical by construction: each function destructures the
context into the same local names at the top, so the ~400-line loop bodies are byte-for-byte
unchanged. Both `#[allow(too_many_arguments)]` attrs removed.
Scoped deliberately: the plan's SessionFactory.build() owning a `vdm.lease -> open_capturer ->
open_encoder -> spawn` RAII chain with Session::drop as the ONLY teardown is coupled to §2.5's
ownership-model rewrite — it needs a host-side VirtualDisplayManager/MonitorLease that doesn't
exist yet (the lifecycle still lives in CURRENT_MON_GEN/IDD_SETUP_LOCK globals + the
per-compositor vdisplay backends). The current teardown is ALREADY drop-based (the capturer owns
the keepalive whose Drop releases the monitor — "restore displays before REMOVE" lives there;
only send_thread.join() is explicit) and is the validated shipping path, so wrapping the deployed
reconfig/switch/rebuild loop in a Session::drop for a behavior-preserving change would add real
regression risk for marginal gain. The SessionFactory/Session::drop/vdm.lease work folds into
§2.5; this stage delivers the concrete, safe arg-bundling.
Verified: Linux cargo check + clippy (-D warnings) + fmt clean. Box build to follow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New src/session_plan.rs: a Copy `SessionPlan { capture, topology, encoder, bit_depth, hdr }`
resolved ONCE from HostConfig (+ the negotiated bit_depth) at the top of `virtual_stream`,
logged, and threaded through build_pipeline_with_retry/build_pipeline. The three scattered
Windows dispatch points now read this one typed artifact instead of re-deriving from config
(plan §2.4, the "capture and encode disagree on the backend" hazard):
* capture: capture::capture_virtual_output takes a CaptureBackend IN (was re-reading
config().idd_push / capture_backend / no_wgc internally). CaptureBackend::resolve() is the
single resolver, shared with the GameStream + spike call sites.
* topology: virtual_stream reads plan.topology; should_use_helper is deleted (its body is
session_plan::resolve_topology, verbatim). The IDD-push reconnect-preempt guard reads
plan.capture too.
* encoder: recorded as EncoderBackend from encode::windows_resolved_backend (config-backed +
GPU-vendor cached since stage 2 -> already a single source). Threading encoder/input_format
into the encoder+capturer opens (which removes the capture->windows_resolved_backend()
back-reference recomputed in dxgi.rs) is stage 5.
Behavior-preserving by construction: each resolved decision is provably equivalent to the
pre-stage-3 reads (same config() + the same cached running_as_system()/GPU-vendor probes), so
old==new. SessionPlan is platform-neutral so it threads the shared virtual_stream/build_pipeline
signatures; on Linux it resolves to the single portal/single-process path.
Also fixes a pre-existing mod-ordering fmt drift in main.rs (mod config; / mod capture;).
Verified: Linux cargo check + clippy (-D warnings) + fmt clean on the touched files. Box build
(Windows compile) + on-glass (NVENC + IDD-push + mode switch) pending on the RTX box.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate 31 genuinely-constant operator/dispatch env::var sites onto HostConfig, so the
capture/topology/encoder decision reads ONE owner instead of being recomputed at each call
site (the latent bug where capture and encode could disagree on the resolved backend, plan §2.4):
idd_push x7, no_wgc, capture_backend, render_adapter, encoder_pref (Linux open_video +
linux_zero_copy_is_vaapi), the Windows vdisplay-backend select, plus the plan-named
secure_dda/idd_depth/zerocopy/ten_bit and the multi-site perf x4 / compositor x5 /
video_source x3 / gamepad. Each HostConfig field's parser is byte-identical to the read it
replaced, so old==new by construction (the plan's "a flipped bool is a silent regression" guard).
Scope correction — the plan's "~64 sites / Linux XDG+compositor included / grep env::var -> 0"
was unsafe as written. Two classes are deliberately KEPT as live reads and documented in config.rs:
* Runtime-mutated session vars. vdisplay::apply_session_env REWRITES the process env on every
connect (the Bazzite Gaming<->Desktop follow): WAYLAND_DISPLAY, XDG_CURRENT_DESKTOP,
XDG_RUNTIME_DIR, DBUS_SESSION_BUS_ADDRESS, and the derived PUNKTFUNK_INPUT_BACKEND,
GAMESCOPE_SESSION/NODE, KWIN/MUTTER_VIRTUAL_PRIMARY, FORCE_SHM. Parsing these once would
freeze them at startup and silently break session-following — they are NOT constant.
* Single-use local tuning with no resolve-once benefit (and FEC_PCT even has two different
semantics): FEC_PCT, VIDEO_DROP, VBV_FRAMES, SPLIT_ENCODE, PACE_BURST_KB, the dxgi timing
knobs, the *_LIVE/test gates, plus path/dynamic reads (config-dir, PATH search,
env-forward-to-child). PUNKTFUNK_ZEROCOPY is split on purpose: Windows presence-semantics
moved to the field; Linux keeps its own truthy (1|true|yes|on) parser.
Verified: Linux cargo check + clippy (-D warnings) + fmt clean on the touched files. The
Windows-only edits are 1:1 substitutions; they get a real Windows compile on the box with Stage 3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
config.rs: typed HostConfig parsed ONCE from env (idd_push/encoder_pref/no_helper/force_helper), replacing per-call env::var re-reads (PUNKTFUNK_ENCODER was re-read on EVERY windows_resolved_backend() call; PUNKTFUNK_IDD_PUSH is read 8x across the host — the recompute that lets capture + encode disagree on the backend, plan §2.4). Migrated the two highest-churn dispatch reads onto it (encode::windows_resolved_backend, punktfunk1::should_use_helper). Behavior-identical: the env is constant for the process lifetime (the service loads host.env before launch), so a lazily-parsed global == parsed-once-at-startup.
docs/windows-host-goal1-plan.md: the ORDERED, independently-shippable execution plan for Goal-1 (the plan's biggest unstarted goal — a from-scratch layered host architecture). Six behavior-preserving, box-verified stages (HostConfig -> SessionPlan -> SessionContext/SessionFactory -> seam-trait tightenings -> src/windows tree), because the host is live-validated and a monolithic rewrite would strand it broken. Stage 1 done here; stages 3-5 rewire the deployed path and require on-glass re-test.
Verified: Linux + box (--features nvenc) cargo check clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>