Skip to content
Open
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
56 changes: 56 additions & 0 deletions docs/research_sweeps/2026-09-22.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Research sweep — 2026-09-22

Window: 2026-09-15 through 2026-09-22. The sweep prioritized source-level changes that were actually committed during the window; projects merely re-indexed by search were not counted as new.

## Promoted: Brush

Source: https://github.com/ArthurBrussee/brush

License: Apache-2.0. Upstream is a cross-platform Rust Gaussian-splat reconstruction/viewer stack using WebGPU-compatible compute through Burn. It can load `.ply` / `.compressed.ply`, supports animated splat archives, runs on Windows/macOS/Linux and can build for web/mobile.

Meaningful updates in-window:

- 2026-09-16: training/rendering work reduced GPU launches, moved scan/radix-sort work into shared GPU kernels, removed avoidable readbacks, and improved fusion of training operations.
- 2026-09-20: migrated more custom operations onto Burn-generated Fusion implementations and added a fusion regression test.

Why it matters here: Brush is a strong candidate for the volumetric-content side of M4/M6 without pulling CUDA-only training dependencies into the Python control runtime. Its viewer is immediately useful for inspecting room/performer splats, and its portable compute direction is aligned with the repository's cross-platform goals.

Implemented in this sweep:

- `projection_mapping.external_tools.build_brush_viewer_command`: deterministic, shell-free command construction.
- `experiments/22_brush_splat_viewer.py`: opt-in adapter that launches a user-installed `brush` binary against a local source or URL.
- no automatic download/install, no dependency changes, and no claim of GPU/hardware validation.
- unit coverage checks path preservation and input validation.

The adapter intentionally stays out-of-process. Brush owns a substantial Rust/WebGPU/Burn stack; embedding it in the Python environment would create dependency and lifecycle coupling for little immediate benefit.

Hardware status: **not validated**. The adapter itself is deterministic; actual Brush rendering, WebGPU support, splat throughput and projector suitability require testing on the target machine.

## Deferred: SuperSplat 3.3.1 / experimental viewport warping

Source: https://github.com/playcanvas/supersplat

Relevant upstream change: 2026-09-16 commit `3ab7965` (“Improve stochastic resolve and add experimental viewport warping”), followed by the 3.3.1 version commit on 2026-09-21.

The change is interesting because it combines stochastic splat rendering with a warped projection path and GPU depth reduction. That is adjacent to our spatial-warp and volumetric-rendering roadmap, but the implementation is tightly coupled to PlayCanvas/TypeScript/WebGPU render passes. Copying it into the ModernGL/Python runtime would be the wrong abstraction and could create licensing/maintenance ambiguity.

Action: keep as a design reference for a future native GPU compositor. In particular, evaluate whether a projector-space warp should happen as a final compositing pass after volumetric rendering, rather than inside each visual mode.

## Deferred: Spark 2.2.x

Source: https://github.com/sparkjsdev/spark

Spark is MIT-licensed and actively maintained; it had distribution/CI updates on 2026-09-21. It provides a Three.js Gaussian-splat renderer with multiple splat formats, multiple viewpoints, dynamic splat transforms, real-time color/displacement/skeletal animation, and a shader graph.

Why deferred: this week's changes were packaging/typecheck maintenance rather than a new rendering capability, and a direct integration would introduce a JavaScript/Three.js runtime beside the existing Python/ModernGL stack. The dynamic-splat shader graph remains worth revisiting if M6 moves toward authored volumetric performers or browser-based remote previews.

## Watched but not counted as new this week

- WebSplatter (MIT): compelling cross-device WebGPU 3DGS architecture with wait-free radix sorting and broad device testing, but its public 1.0.0 archive predates this sweep window. Keep it as a WebGPU architecture reference rather than presenting it as a weekly discovery.
- Ghost Arcade: highly relevant projection/VJ feature set, but the public source history inspected for this sweep did not show a qualifying new source update in the window. Its AGPL licensing also makes code-level reuse a deliberate legal/architecture decision; use it as product/UX reference unless that decision is made explicitly.

## Follow-up candidates

1. Add a registry feature for the Brush adapter only after deciding how external executable requirements should be represented in `features.toml` (current dependency checks are Python-module oriented).
2. Prototype a renderer-agnostic final warp/composite contract under M2/M4, informed by SuperSplat's separate warped render target rather than copying its implementation.
3. Test Brush on the actual projection workstation with a small `.ply` and record GPU/backend, FPS, memory, launch/exit behavior and fullscreen/projector routing before promoting it beyond “experimental adapter”.
58 changes: 58 additions & 0 deletions experiments/22_brush_splat_viewer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from __future__ import annotations

import argparse
import sys

from projection_mapping.external_tools import (
build_brush_viewer_command,
find_executable,
launch_external,
)


def main() -> int:
ap = argparse.ArgumentParser(
description="Launch a user-installed Brush WebGPU Gaussian-splat viewer out-of-process."
)
ap.add_argument("source", help="PLY/compressed PLY, dataset path, or URL accepted by Brush")
ap.add_argument("--brush", default="brush", help="Brush executable name/path")
ap.add_argument(
"--dry-run",
action="store_true",
help="Validate and print the command without launching Brush",
)
args, extra = ap.parse_known_args()

command = build_brush_viewer_command(args.source, executable=args.brush, extra_args=extra)
if args.dry_run:
print("[brush-adapter]", " ".join(command.argv))
return 0

resolved = find_executable(command.executable)
if resolved is None:
print(
"[brush-adapter] Brush is not installed or is not on PATH. "
"Install/build Brush separately, then rerun this experiment. "
"The ProjectionMapping environment is intentionally not modified.",
file=sys.stderr,
)
return 2

print(
"[brush-adapter] launching external Brush viewer; WebGPU/GPU behavior is upstream- and hardware-dependent",
flush=True,
)
process = launch_external(command)
try:
return int(process.wait())
except KeyboardInterrupt:
process.terminate()
try:
return int(process.wait(timeout=5.0))
except Exception:
process.kill()
return int(process.wait())


if __name__ == "__main__":
raise SystemExit(main())
43 changes: 43 additions & 0 deletions src/projection_mapping/external_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
import shutil
import subprocess
from typing import Sequence


@dataclass(frozen=True)
class ExternalToolCommand:
argv: tuple[str, ...]
executable: str


def build_brush_viewer_command(
source: str | Path,
*,
executable: str = "brush",
extra_args: Sequence[str] = (),
) -> ExternalToolCommand:
"""Build a shell-free Brush viewer command.

Brush is intentionally kept out-of-process: it is a Rust/WebGPU application
with its own dependency/runtime stack. The adapter only launches a user-
installed Brush binary and never downloads, installs, or executes a shell.
"""
source_text = str(source).strip()
if not source_text:
raise ValueError("Brush source path/URL must not be empty")
if not executable.strip():
raise ValueError("Brush executable must not be empty")
argv = (executable, source_text, "--with-viewer", "true", *tuple(extra_args))
return ExternalToolCommand(argv=argv, executable=executable)


def find_executable(name: str) -> str | None:
return shutil.which(name)


def launch_external(command: ExternalToolCommand) -> subprocess.Popen[bytes]:
"""Launch an external tool without a shell so paths/URLs stay literal."""
return subprocess.Popen(command.argv, shell=False)
24 changes: 24 additions & 0 deletions tests/test_external_tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from __future__ import annotations

import pytest

from projection_mapping.external_tools import build_brush_viewer_command


def test_brush_command_is_shell_free_and_preserves_source() -> None:
source = "captures/room scan/scene.ply"
command = build_brush_viewer_command(source, extra_args=("--some-upstream-flag", "7"))
assert command.executable == "brush"
assert command.argv == (
"brush",
source,
"--with-viewer",
"true",
"--some-upstream-flag",
"7",
)


def test_brush_command_rejects_empty_source() -> None:
with pytest.raises(ValueError):
build_brush_viewer_command(" ")
Loading