gsplat 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +202 -0
  3. data/README.md +236 -0
  4. data/docs/ACCEPTANCE.md +60 -0
  5. data/docs/BENCHMARKS.md +79 -0
  6. data/docs/DECISIONS.md +52 -0
  7. data/docs/MIGRATION.md +117 -0
  8. data/docs/PROFILE.md +75 -0
  9. data/docs/PROGRESS.md +199 -0
  10. data/docs/decisions/0000-template.md +17 -0
  11. data/docs/decisions/0001-pin-golden-reference-and-cpu-validation.md +27 -0
  12. data/docs/decisions/0002-retain-ruby-fallbacks-for-native-backend.md +26 -0
  13. data/docs/decisions/0003-share-projection-for-distorted-cameras.md +26 -0
  14. data/docs/decisions/0004-use-portable-world-space-reference-paths.md +26 -0
  15. data/docs/decisions/0005-reuse-ewa-core-for-2dgs.md +24 -0
  16. data/docs/decisions/0006-keep-eval3d-as-portable-reference.md +25 -0
  17. data/docs/decisions/0007-share-compositor-semantics-for-contribution-indices.md +25 -0
  18. data/examples/data/README.md +14 -0
  19. data/examples/data/colmap/images/view_000.png +0 -0
  20. data/examples/data/colmap/images/view_001.png +0 -0
  21. data/examples/data/colmap/images/view_002.png +0 -0
  22. data/examples/data/colmap/sparse/0/cameras.txt +2 -0
  23. data/examples/data/colmap/sparse/0/images.txt +6 -0
  24. data/examples/data/colmap/sparse/0/points3D.txt +17 -0
  25. data/examples/data/splats.ply +0 -0
  26. data/examples/fit_image.rb +35 -0
  27. data/examples/generate_sample_data.rb +147 -0
  28. data/examples/render_path.rb +117 -0
  29. data/examples/simple_trainer.rb +76 -0
  30. data/ext/gsplat_native/common.h +54 -0
  31. data/ext/gsplat_native/extconf.rb +39 -0
  32. data/ext/gsplat_native/gsplat_native.c +60 -0
  33. data/ext/gsplat_native/intersections.c +211 -0
  34. data/ext/gsplat_native/projection.c +199 -0
  35. data/ext/gsplat_native/raster_backward.c +129 -0
  36. data/ext/gsplat_native/raster_backward_bridge.c +69 -0
  37. data/ext/gsplat_native/raster_forward.c +150 -0
  38. data/ext/gsplat_native/rasterization.h +51 -0
  39. data/ext/gsplat_native/spherical_harmonics.c +129 -0
  40. data/gsplat.gemspec +36 -0
  41. data/lib/gsplat/autograd/context.rb +60 -0
  42. data/lib/gsplat/autograd/function.rb +68 -0
  43. data/lib/gsplat/autograd/variable.rb +159 -0
  44. data/lib/gsplat/backend/ruby/accumulate.rb +139 -0
  45. data/lib/gsplat/backend/ruby/accumulate_backward.rb +40 -0
  46. data/lib/gsplat/backend/ruby/eval3d_rasterizer.rb +175 -0
  47. data/lib/gsplat/backend/ruby/isect_tiles.rb +198 -0
  48. data/lib/gsplat/backend/ruby/projection.rb +251 -0
  49. data/lib/gsplat/backend/ruby/projection_backward.rb +190 -0
  50. data/lib/gsplat/backend/ruby/projection_covariance_vjp.rb +72 -0
  51. data/lib/gsplat/backend/ruby/projection_input_vjp.rb +252 -0
  52. data/lib/gsplat/backend/ruby/quat_scale_to_covar_preci.rb +139 -0
  53. data/lib/gsplat/backend/ruby/rasterize_to_indices_in_range.rb +121 -0
  54. data/lib/gsplat/backend/ruby/rasterize_to_pixels.rb +199 -0
  55. data/lib/gsplat/backend/ruby/rasterize_to_pixels_backward.rb +121 -0
  56. data/lib/gsplat/backend/ruby/spherical_harmonics.rb +135 -0
  57. data/lib/gsplat/backend/ruby/tile_compositor.rb +50 -0
  58. data/lib/gsplat/backend/ruby/tile_compositor_backward.rb +84 -0
  59. data/lib/gsplat/backend.rb +79 -0
  60. data/lib/gsplat/compression/grid_sort.rb +79 -0
  61. data/lib/gsplat/compression/kmeans.rb +121 -0
  62. data/lib/gsplat/compression/png.rb +147 -0
  63. data/lib/gsplat/compression/png_codec.rb +133 -0
  64. data/lib/gsplat/compression/quantizer.rb +79 -0
  65. data/lib/gsplat/io/checkpoint.rb +145 -0
  66. data/lib/gsplat/io/colmap.rb +175 -0
  67. data/lib/gsplat/io/colmap_binary.rb +98 -0
  68. data/lib/gsplat/io/colmap_text.rb +84 -0
  69. data/lib/gsplat/io/image.rb +63 -0
  70. data/lib/gsplat/io/image_backends.rb +81 -0
  71. data/lib/gsplat/io/npy.rb +189 -0
  72. data/lib/gsplat/io/ply.rb +185 -0
  73. data/lib/gsplat/io/ply_reader.rb +142 -0
  74. data/lib/gsplat/io/zip_archive.rb +183 -0
  75. data/lib/gsplat/math/camera_distortion.rb +123 -0
  76. data/lib/gsplat/math/camera_projection.rb +202 -0
  77. data/lib/gsplat/math/mat.rb +114 -0
  78. data/lib/gsplat/math/quaternion.rb +175 -0
  79. data/lib/gsplat/math/small_matrix_primitives.rb +112 -0
  80. data/lib/gsplat/math/spherical_harmonic_basis.rb +148 -0
  81. data/lib/gsplat/math/ssim.rb +153 -0
  82. data/lib/gsplat/native.rb +30 -0
  83. data/lib/gsplat/native_ops.rb +148 -0
  84. data/lib/gsplat/native_raster_ops.rb +76 -0
  85. data/lib/gsplat/ops/accumulate.rb +68 -0
  86. data/lib/gsplat/ops/eval3d_rasterize.rb +62 -0
  87. data/lib/gsplat/ops/isect_tiles.rb +38 -0
  88. data/lib/gsplat/ops/projection.rb +154 -0
  89. data/lib/gsplat/ops/quat_scale_to_covar_preci.rb +118 -0
  90. data/lib/gsplat/ops/rasterize_to_indices_in_range.rb +38 -0
  91. data/lib/gsplat/ops/rasterize_to_pixels.rb +105 -0
  92. data/lib/gsplat/ops/relocation.rb +94 -0
  93. data/lib/gsplat/ops/spherical_harmonics.rb +71 -0
  94. data/lib/gsplat/ops/tensor_shape_ops.rb +173 -0
  95. data/lib/gsplat/ops/tensor_value_ops.rb +150 -0
  96. data/lib/gsplat/optim/adam.rb +212 -0
  97. data/lib/gsplat/optim/lr_scheduler.rb +36 -0
  98. data/lib/gsplat/optim/selective_adam.rb +68 -0
  99. data/lib/gsplat/rasterization.rb +130 -0
  100. data/lib/gsplat/rasterization_2dgs.rb +140 -0
  101. data/lib/gsplat/rasterization_helpers.rb +162 -0
  102. data/lib/gsplat/rasterization_validation.rb +99 -0
  103. data/lib/gsplat/strategy/base.rb +49 -0
  104. data/lib/gsplat/strategy/default.rb +188 -0
  105. data/lib/gsplat/strategy/mcmc.rb +103 -0
  106. data/lib/gsplat/strategy/mcmc_ops.rb +143 -0
  107. data/lib/gsplat/strategy/ops.rb +165 -0
  108. data/lib/gsplat/training/config.rb +91 -0
  109. data/lib/gsplat/training/image_fitter.rb +158 -0
  110. data/lib/gsplat/training/losses.rb +191 -0
  111. data/lib/gsplat/training/scene.rb +116 -0
  112. data/lib/gsplat/training/trainer.rb +236 -0
  113. data/lib/gsplat/utils.rb +110 -0
  114. data/lib/gsplat/version.rb +6 -0
  115. data/lib/gsplat.rb +98 -0
  116. metadata +181 -0
data/docs/MIGRATION.md ADDED
@@ -0,0 +1,117 @@
1
+ # Migrating from Python gsplat
2
+
3
+ The Ruby API keeps upstream operation names and tensor layouts where Numo::NArray can represent them
4
+ directly. The main changes are Ruby keyword syntax, an explicit lightweight autograd wrapper, and a
5
+ CPU-only dense execution model.
6
+
7
+ ## Rendering call
8
+
9
+ Python:
10
+
11
+ ```python
12
+ rendered, alphas, meta = rasterization(
13
+ means, quats, scales, opacities, colors, viewmats, Ks, width, height,
14
+ render_mode="RGB", packed=False,
15
+ )
16
+ ```
17
+
18
+ Ruby:
19
+
20
+ ```ruby
21
+ rendered, alphas, meta = Gsplat.rasterization(
22
+ means: means,
23
+ quats: quats,
24
+ scales: scales,
25
+ opacities: opacities,
26
+ colors: colors,
27
+ viewmats: viewmats,
28
+ ks: intrinsics,
29
+ width: width,
30
+ height: height,
31
+ render_mode: "RGB",
32
+ packed: false
33
+ )
34
+ ```
35
+
36
+ `Ks` becomes the idiomatic keyword `ks`. Hash keys in metadata are symbols, for example
37
+ `meta[:means2d]`, `meta[:radii]`, and `meta[:isect_offsets]`.
38
+
39
+ ## Tensor and autograd mapping
40
+
41
+ | PyTorch | Ruby |
42
+ |---|---|
43
+ | `torch.float32` / `torch.float64` | `Numo::SFloat` / `Numo::DFloat` |
44
+ | `tensor.requires_grad_(True)` | `Autograd::Variable.new(array, requires_grad: true)` |
45
+ | `tensor.detach()` | `variable.data` |
46
+ | `tensor.grad` | `variable.grad` |
47
+ | `optimizer.zero_grad()` | `optimizer.zero_grad!` or `variable.zero_grad!` |
48
+ | `with torch.no_grad()` | `Gsplat::Autograd.no_grad { ... }` |
49
+ | `loss.backward()` | `loss.backward` |
50
+ | `output.backward(weight)` | `output.backward(weight)` |
51
+
52
+ Numo arrays do not carry device or graph state. Only `Autograd::Variable` does. Mixed calls return
53
+ Variables when any differentiable input requires gradients, otherwise they return raw Numo arrays.
54
+
55
+ The package supports camera batches `[C,...]`, not arbitrary leading batch dimensions. It uses dense
56
+ `[C,N,...]` intermediates. `packed: true` is accepted for call compatibility but warns and executes
57
+ dense mode; sparse gradients are unavailable.
58
+
59
+ ## Geometry and activation
60
+
61
+ - Quaternion order is `wxyz`.
62
+ - Rendering takes activated `scales > 0` and `opacities` in the 0–1 range.
63
+ - Trainer parameter hashes store `:scales` as log-scales and `:opacities` as logits, matching the
64
+ optimization convention in the upstream examples.
65
+ - Direct `covars [N,3,3]` or packed symmetric `covars [N,6]` replace `quats`/`scales` on the normal
66
+ 2D raster path.
67
+ - Views are world-to-camera `[C,4,4]`; camera forward is positive Z.
68
+
69
+ ## Render modes and metadata
70
+
71
+ The following modes are available: `RGB`, `D`, `ED`, `RGB+D`, `RGB+ED`, `d`, `Ed`, `RGB-d`, and
72
+ `RGB-Ed`. Uppercase depth uses projected Gaussian depth; lowercase depth is per-ray hit distance.
73
+
74
+ `absgrad: true` updates `meta[:means2d_absgrad]` after backward. World-space normals requested through
75
+ `with_eval3d: true, return_normals: true` are returned as `meta[:normals]`.
76
+
77
+ ## Optimizers and strategies
78
+
79
+ An upstream parameter dictionary maps naturally to a Ruby symbol-keyed Hash:
80
+
81
+ ```ruby
82
+ params = {
83
+ means: means_variable,
84
+ quats: quats_variable,
85
+ scales: log_scales_variable,
86
+ opacities: logits_variable,
87
+ sh0: direct_sh_variable,
88
+ shN: remaining_sh_variable
89
+ }
90
+ ```
91
+
92
+ `Gsplat::Strategy::Default` and `Gsplat::Strategy::MCMC` use the same lifecycle around backward. All
93
+ structural edits must go through `Gsplat::Strategy::Ops` so parameter rows and Adam moments remain
94
+ aligned. `Gsplat::Optim::SelectiveAdam` accepts a first-axis visibility mask for visible-only updates.
95
+
96
+ ## IO mapping
97
+
98
+ | Python workflow | Ruby API |
99
+ |---|---|
100
+ | COLMAP parser | `Gsplat::IO::Colmap.read` |
101
+ | Inria PLY export/import | `Gsplat::IO::Ply.write` / `.read` |
102
+ | training checkpoint | `Gsplat::IO::Checkpoint.save` / `.restore!` |
103
+ | NumPy fixture exchange | `Gsplat::IO::Npy.read`, `.write`, `.read_npz`, `.write_npz` |
104
+ | PNG parameter compression | `Gsplat::Compression::Png#compress` / `#decompress` |
105
+
106
+ NPY/NPZ supports the dtypes used by this project and C-order arrays. It intentionally does not load
107
+ arbitrary NumPy object arrays or Fortran-order payloads.
108
+
109
+ ## Unsupported upstream options
110
+
111
+ There is no CUDA device backend, distributed rendering, arbitrary leading batch support, packed/sparse
112
+ execution, F-theta/lidar cameras, rolling shutter, custom rays, unscented-transform projection,
113
+ renderer configs, extra signals, or LPIPS in version 1.0.
114
+
115
+ The current 2DGS auxiliary buffers are compatible in shape but approximate upstream ray-splat
116
+ geometry. Eval3d is pinhole/classic only and uses a numerical geometry VJP. Check
117
+ `docs/DECISIONS.md` when exact cross-implementation parity is required.
data/docs/PROFILE.md ADDED
@@ -0,0 +1,75 @@
1
+ # Performance Profile
2
+
3
+ ## Ruby baseline — 2026-07-23
4
+
5
+ Command:
6
+
7
+ ```bash
8
+ bundle exec ruby -Ilib benchmarks/profile_ruby.rb
9
+ ```
10
+
11
+ Environment: CRuby 4.0.0, arm64-darwin24. Deterministic scene: 1,000 Gaussians, one 64×48
12
+ camera, SH degree 3, five measured iterations after one warm-up.
13
+
14
+ | Operation | Mean time |
15
+ |---|---:|
16
+ | Projection | 1.010 ms |
17
+ | Spherical harmonics | 0.393 ms |
18
+ | Tile intersections and sort | 2.505 ms |
19
+ | Rasterization forward | 58.350 ms |
20
+
21
+ Rasterization accounts for about 93.7% of the summed operation time. Tile intersection generation
22
+ is the second-largest component. This satisfies the P10 profiling prerequisite and fixes the native
23
+ implementation order: bridge first, projection/SH for coverage, intersection sort, then the
24
+ dominant raster forward/backward path. These numbers are a small-scene attribution profile, not
25
+ the final P12 throughput benchmark.
26
+
27
+ ## Native projection and SH — 2026-07-23
28
+
29
+ With the P10-2 extension loaded (`GSPLAT_BACKEND=auto`) on the same workload:
30
+
31
+ | Operation | Ruby | Native/auto | Speedup |
32
+ |---|---:|---:|---:|
33
+ | Projection | 1.010 ms | 0.257 ms | 3.9× |
34
+ | Spherical harmonics | 0.393 ms | 0.043 ms | 9.1× |
35
+
36
+ The C paths cover contiguous float32 forward calls. Float64, masked SH, and analytic backward use
37
+ the Ruby implementation to preserve the existing numerical behavior.
38
+
39
+ ## Native intersections — 2026-07-23
40
+
41
+ The P10-3 kernel adds float32 tile-bound enumeration, 64-bit key generation, stable LSD radix sort,
42
+ and tile-offset encoding. It retains the same key layout and ordering as the Ruby implementation.
43
+
44
+ | Operation | Ruby | Native/auto | Speedup |
45
+ |---|---:|---:|---:|
46
+ | Tile intersections and sort | 2.505 ms | 0.045 ms | 55.7× |
47
+
48
+ The measurement uses the same 1,000-Gaussian workload as the baseline. Float64 inputs retain the
49
+ Ruby path so numerical gradient tests continue to exercise the reference implementation.
50
+
51
+ ## Native rasterization — 2026-07-23
52
+
53
+ The float32 forward and backward tile kernels release the GVL. Forward owns disjoint pixel tiles.
54
+ Backward uses atomic scatter-add under OpenMP; this keeps memory at `O(N × D)` rather than allocating
55
+ one full gradient buffer per worker. On the 1,000-Gaussian, 64×48 profile:
56
+
57
+ | Operation | Ruby | Native (1 thread) | Speedup |
58
+ |---|---:|---:|---:|
59
+ | Raster forward | 55.821 ms | 2.284 ms | 24.4× |
60
+ | Raster backward + absgrad | 136.854 ms | 2.700 ms | 50.7× |
61
+
62
+ The required 100,000-Gaussian, 800×800 float32 workload gives:
63
+
64
+ | OpenMP threads | Forward | Backward + absgrad | Combined |
65
+ |---:|---:|---:|---:|
66
+ | 1 | 114.012 ms | 279.790 ms | 393.802 ms |
67
+ | 2 | 57.751 ms | 148.111 ms | 205.862 ms |
68
+ | 4 | 30.446 ms | 83.138 ms | 113.584 ms |
69
+ | 8 | 29.643 ms | 79.252 ms | 108.895 ms |
70
+
71
+ This meets the 150 ms forward and 400 ms combined design targets even at one thread. The measured
72
+ 3.5× backward scaling from one to eight workers supports the bounded-memory atomic strategy. Scaling
73
+ flattens after four workers on this Apple Silicon host, so higher thread counts are not assumed to
74
+ improve throughput. Homebrew `libomp` is auto-detected on Apple Clang; other builds remain functional
75
+ without OpenMP.
data/docs/PROGRESS.md ADDED
@@ -0,0 +1,199 @@
1
+ # Implementation Progress
2
+
3
+ This file is the restart point for implementation sessions. Read it after `README.md`.
4
+
5
+ | Task | Status | Completed | Notes |
6
+ |---|---|---|---|
7
+ | P0-1 | Complete | 2026-07-23 | Gem foundation; 4 tests, 7 assertions |
8
+ | P0-2 | Complete | 2026-07-23 | Backend registry, selection, environment override, one-time fallback warning |
9
+ | P0-3 | Complete | 2026-07-23 | NPY v1.0 and stored/deflated NPZ; NumPy 2.3.1 interoperability verified |
10
+ | P0-4 | Complete (alternate DoD) | 2026-07-23 | 47-case generator and dependency-free dry run; CUDA goldens pending CUDA host |
11
+ | P0-5 | Complete | 2026-07-23 | allclose/golden/backend test helpers and Ruby 3.2–4.0 CI matrix |
12
+ | P1-1 | Complete | 2026-07-23 | Autograd Variable/Function/Context, branching, multi-output, no_grad, graph release |
13
+ | P1-2 | Complete | 2026-07-23 | Batched quaternion VJPs and closed-form 2x2/3x3 matrix operations |
14
+ | P1-3 | Complete (golden pending) | 2026-07-23 | Covariance/precision fwd+bwd, triu output, float64 gradcheck |
15
+ | P2-1 | Complete (golden pending) | 2026-07-23 | SH degrees 0–4, masks, arbitrary channels, analytic direction/coefficient VJPs |
16
+ | P3-1 | Complete (golden pending) | 2026-07-23 | Pinhole forward, world/camera primitives, culling and compensation |
17
+ | P3-2 | Complete (golden pending) | 2026-07-23 | Analytic projection VJPs for means, covariance, quaternion and scale |
18
+ | P3-3 | Complete (golden pending) | 2026-07-23 | Orthographic forward/backward and camera-model dispatch |
19
+ | P4-1 | Complete (golden pending) | 2026-07-23 | Tile AABBs, 64-bit intersection keys, sorting and prefix offsets |
20
+ | P5-1 | Complete | 2026-07-23 | Brute-force alpha compositor with arbitrary channels and backgrounds |
21
+ | P5-2 | Complete (golden pending) | 2026-07-23 | Vectorized tile compositor, masks, partial edge tiles and retained last ids |
22
+ | P6-1 | Complete (golden pending) | 2026-07-23 | Reverse tile scan VJPs and brute-force gradient cross-check |
23
+ | P6-2 | Complete (golden pending) | 2026-07-23 | Optional per-pixel absolute mean-gradient accumulation |
24
+ | P7-1 | Complete (golden pending) | 2026-07-23 | High-level render modes, SH, antialiasing, depth normalization and metadata |
25
+ | P7-2 | Complete | 2026-07-23 | Self-consistency image fitter, 128px fixture and monotonic PSNR E2E |
26
+ | P8-1 | Complete | 2026-07-23 | Adam groups, bias correction, editable state and exponential scheduling |
27
+ | P8-2 | Complete | 2026-07-23 | Strategy lifecycle and synchronized duplicate/split/remove/reset operations |
28
+ | P8-3 | Complete (golden pending) | 2026-07-23 | Default densification statistics, growth, pruning and opacity reset |
29
+ | P8-4 | Complete (golden pending) | 2026-07-23 | Closed-form relocation, weighted sampling, covariance noise and MCMC strategy |
30
+ | P9-1 | Complete | 2026-07-23 | Inria PLY writer and arbitrary-order ASCII/binary little-endian reader |
31
+ | P9-2 | Complete | 2026-07-23 | COLMAP bin/txt cameras, images and 100-point fixture with pose conversion |
32
+ | P9-3 | Complete (golden pending) | 2026-07-23 | k-NN initialization, SH color helpers, scene scale and differentiable SSIM |
33
+ | P9-4 | Complete | 2026-07-23 | NPZ training checkpoints and vips/chunky_png RGB image IO |
34
+ | P9-5 | Complete | 2026-07-23 | Multi-view Trainer, staged SH, strategy hooks and training/render CLIs |
35
+ | P10-1 | Complete | 2026-07-23 | Optional C extension, Numo bridge, GVL release and build fallback |
36
+ | P10-2 | Complete (hybrid) | 2026-07-23 | C float32 projection/SH forward; analytic backward and float64 fallback |
37
+ | P10-3 | Complete | 2026-07-23 | C tile enumeration, stable 64-bit radix sort and offset encoding |
38
+ | P10-4 | Complete | 2026-07-23 | GVL-free OpenMP raster forward/backward with atomic scatter-add |
39
+ | P11-1 | Complete (golden pending) | 2026-07-23 | N-D features, differentiable channel chunking and radius clipping |
40
+ | P11-2 | Complete (golden pending) | 2026-07-23 | Full and packed direct covariance forward/backward across the renderer |
41
+ | P11-3 | Complete (golden pending) | 2026-07-23 | Axis-aligned elliptical radii in Ruby/native projection and metadata |
42
+ | P11-4 | Complete | 2026-07-23 | Morton grid sort, PNG quantization, SH K-means and compatible metadata layout |
43
+ | P11-5 | Complete (golden pending) | 2026-07-23 | Fisheye and OpenCV radial/tangential/thin-prism projection with VJPs |
44
+ | P11-6 | Complete (golden pending) | 2026-07-24 | Euclidean center sorting and per-pixel anisotropic ray-hit distance modes |
45
+ | P11-7 | Complete | 2026-07-24 | First-axis visibility-masked Adam parameter and moment updates |
46
+ | P11-8 | Complete (golden pending) | 2026-07-24 | 2DGS API, auxiliary geometry buffers and Trainer mode |
47
+ | P11-9 | Complete (golden pending) | 2026-07-24 | World-space color evaluation, ray-facing accumulated normals and numerical VJPs |
48
+ | P11-10 | Complete (golden pending) | 2026-07-24 | Iterative per-pixel Gaussian contribution-index enumeration |
49
+ | P12-1 | Complete | 2026-07-24 | README/Migration guide, executable quick start, example smoke tests and 100% public YARD coverage |
50
+ | P12-2 | Complete (external gates recorded) | 2026-07-24 | Design workloads, benchmark results, enabled CI matrix and G1–G6 acceptance report |
51
+ | P12-3 | Complete | 2026-07-24 | Version 1.0.0, Apache-2.0, release metadata, gem build and clean native install/render verified |
52
+
53
+ ## Phase gates
54
+
55
+ ### P0 — Complete (alternate golden-data gate)
56
+
57
+ - Full suite: 25 tests, 91 assertions, no failures or skips.
58
+ - RuboCop: no offenses.
59
+ - Golden status: generator dry-run and syntax checks pass; complete CUDA output remains pending per
60
+ `tools/README.md` because this host has no CUDA and the temporary PyTorch install exceeded disk capacity.
61
+ - Design differences: recorded in `docs/DECISIONS.md`.
62
+
63
+ ### P1 — Complete (golden-data gate pending)
64
+
65
+ - L1/L2: analytic properties and float64 central differences pass.
66
+ - L3: `quat_covar_full.npz` test is present and skipped until `tools/README.md` CUDA generation is run.
67
+ - Full suite and lint evidence are recorded in the P1-3 commit.
68
+
69
+ ### P2 — Complete (golden-data gate pending)
70
+
71
+ - L1/L2: known degree-zero/degree-one values and float64 direction/coefficient central differences pass.
72
+ - Masks suppress forward values and their corresponding VJPs; channels are not restricted to RGB.
73
+ - L3: `sh_deg3.npz` test is present and skipped until `tools/README.md` golden generation is run.
74
+ - Full suite: 52 tests, 130 assertions, no failures, 2 documented golden-data skips.
75
+ - RuboCop: no offenses.
76
+
77
+ ### P3 — Complete (golden-data gate pending)
78
+
79
+ - L1/L2: hand-calculated pinhole/orthographic cases and float64 central differences pass.
80
+ - Covariance-direct and quaternion/scale paths both propagate analytic gradients.
81
+ - L3: pinhole and orthographic tests are present and skip until golden generation is run.
82
+ - Projection radii use the upstream `[C,N,2]` axis-aligned elliptical representation.
83
+ - Full suite: 63 tests, 155 assertions, no failures, 5 documented golden-data skips.
84
+ - RuboCop: no offenses.
85
+
86
+ ### P4 — Complete (golden-data gate pending)
87
+
88
+ - L1: exact handcrafted tile counts, 64-bit keys, flattened ids and empty-tile offsets pass.
89
+ - Elliptical radii are primary; legacy scalar radii remain accepted by the low-level intersection API.
90
+ - L3: `isect_c3_n1000.npz` coverage is present and skips until golden generation is run.
91
+ - Full suite: 68 tests, 163 assertions, no failures, 6 documented golden-data skips.
92
+ - RuboCop: no offenses.
93
+
94
+ ### P5 — Complete (golden-data gate pending)
95
+
96
+ - L1/L4: single-pixel analytical cases and tile-vs-brute-force image comparisons pass.
97
+ - Partial edge tiles, arbitrary channels, backgrounds and tile masks are covered.
98
+ - L3: `raster_rgb.npz` forward coverage is present and skips until CUDA golden generation is run.
99
+ - Full suite: 75 tests, 175 assertions, no failures, 7 documented golden-data skips.
100
+ - RuboCop: no offenses.
101
+
102
+ ### P6 — Complete (golden-data gate pending)
103
+
104
+ - L2: float64 central differences pass for means, conics, colors, opacities and backgrounds.
105
+ - L4: tile and brute-force backward gradients match exactly on the reference scene.
106
+ - `absgrad` accumulates absolute per-pixel mean contributions and allocates no buffer when disabled.
107
+ - L3: raster gradients and absolute gradients skip until CUDA golden generation is run.
108
+ - Full suite: 81 tests, 190 assertions, no failures, 9 documented golden-data skips.
109
+ - RuboCop: no offenses.
110
+
111
+ ### P7 — Complete (golden-data gate pending)
112
+
113
+ - High-level RGB/depth modes, SH, antialiasing, metadata and end-to-end gradients pass.
114
+ - The reduced image-fit E2E improves monotonically from 11.77 dB to 28.89 dB in 12 steps.
115
+ - A 128×128 PPM fixture and a configurable 2,000-Gaussian/300-step example are included.
116
+ - L3 render fixtures remain pending CUDA golden generation.
117
+ - Full suite: 88 tests, 248 assertions, no failures, 9 documented golden-data skips.
118
+ - RuboCop: no offenses.
119
+
120
+ ### P8 — Complete (golden-data gate pending)
121
+
122
+ - Adam bias correction, exponential scheduling and synchronized structural state edits pass.
123
+ - Default strategy growth/pruning and MCMC relocation/growth/noise paths are covered.
124
+ - MCMC equation 9 is verified analytically; noise mean/variance and a reduced image fit pass.
125
+ - L3 strategy and relocation fixtures remain pending CUDA golden generation.
126
+ - Full suite: 109 tests, 323 assertions, no failures, 11 documented golden-data skips.
127
+ - RuboCop: no offenses.
128
+
129
+ ### P9 — Complete (golden-data gate pending)
130
+
131
+ - Inria PLY and COLMAP bin/txt parsing pass round-trip and paired-fixture tests.
132
+ - k-NN initialization, scene scale, L1/SSIM/PSNR, checkpoint restart, and optional image backends pass.
133
+ - The eight-view synthetic Trainer E2E exceeds 28 dB in 80 steps; the production default remains 30,000 steps.
134
+ - `simple_trainer.rb` and `render_path.rb` provide the documented real-COLMAP workflow and bundled sample data.
135
+ - L3 SSIM remains pending golden generation; the source bundle uses chunky_png to exercise image IO.
136
+ - Full suite: 131 tests, 817 assertions, no failures, 14 documented skips.
137
+ - RuboCop: no offenses.
138
+
139
+ ### P10-3 — Complete
140
+
141
+ - Native float32 intersections exactly match the Ruby keys, flattened ids, counts and offsets.
142
+ - The stable eight-pass radix sort preserves key/id pairing and runs without the Ruby GVL.
143
+ - The 1,000-Gaussian profile improves from 2.505 ms to 0.045 ms (55.7×).
144
+ - Full suite: 136 tests, 836 assertions, no failures, 14 documented skips.
145
+ - RuboCop: no offenses.
146
+
147
+ ### P10 — Complete
148
+
149
+ - Contiguous float32 projection, SH, intersections, raster forward and raster backward use C kernels.
150
+ - Raster forward owns disjoint tiles; backward uses bounded-memory atomic scatter-add and supports
151
+ arbitrary channels, backgrounds, masks and absolute mean gradients.
152
+ - At 100k Gaussians and 800×800, 8-thread forward is 29.643 ms and combined forward/backward is
153
+ 108.895 ms, within the 150/400 ms design targets.
154
+ - The explicit `:native` suite passes with one and eight OpenMP threads.
155
+ - Full suite: 137 tests, 845 assertions, no failures, 14 documented skips.
156
+ - RuboCop: no offenses.
157
+
158
+ ### P11 — Complete (golden-data gate pending)
159
+
160
+ - Extended rendering covers feature chunking, direct covariance, elliptical radii, distorted/fisheye
161
+ cameras, distance modes, eval3d normals, 2DGS, SelectiveAdam, and contribution enumeration.
162
+ - Each extension has analytic/property coverage and a pinned Python generator case where upstream
163
+ execution is required. CUDA fixtures remain the documented external gate.
164
+ - Both backend selections pass the same 181-test, 977-assertion suite with 22 documented skips.
165
+ - RuboCop: no offenses.
166
+
167
+ ### P12-1 — Complete
168
+
169
+ - README includes installation, executable quick start, conventions, API mapping, feature status,
170
+ limitations, and the three production examples. `docs/MIGRATION.md` covers Python migration.
171
+ - `test/readme_test.rb` executes the exact quick-start fence. Example smoke tests run a real image-fit
172
+ step, parse all COLMAP fixture records, and validate a PLY orbit setup without optional image gems.
173
+ - `yard stats`: 40 files, 129 methods, 124 attributes, 22 constants, undocumented 0 (100%).
174
+ - Ruby and native selections: 186 tests, 1,005 assertions, no failures, 22 documented skips.
175
+ - RuboCop: 130 files, no offenses.
176
+
177
+ ### P12-2 — Complete (external validation pending)
178
+
179
+ - The 100k/800×800 workload passes all timing targets: Ruby 3,527.990/8,047.024 ms and
180
+ native 30.419/112.573 ms for forward/combined forward+backward.
181
+ - The full 50k/512²/2,000-step native image fit completes in 157.457 seconds versus the
182
+ 30-minute target. The COLMAP benchmark path completes a one-step fixture smoke run.
183
+ - `docs/BENCHMARKS.md` records commands and limitations; `docs/ACCEPTANCE.md` maps G1–G6 to evidence.
184
+ CUDA golden parity (G1/G2) and real-capture 30k quality (G4) remain explicit external gates.
185
+ - CI now runs Ruby 3.2–4.0, native build/full suite, documentation, and focused E2E jobs.
186
+ - Ruby and native selections: 188 tests, 1,041 assertions, no failures, 22 documented skips.
187
+ - YARD: undocumented 0 (100%). RuboCop: 132 files, no offenses. Workflow YAML parses successfully.
188
+
189
+ ### P12-3 — Complete
190
+
191
+ - Version 1.0.0, release metadata, a bounded runtime dependency, and the release file manifest are
192
+ covered by `test/gemspec_test.rb`.
193
+ - `gem build gsplat.gemspec` produces `gsplat-1.0.0.gem`. Installing that artifact into an empty
194
+ temporary gem home compiles and loads the native extension, then completes a 1×1 render without
195
+ loading files from the working tree.
196
+ - Ruby and native selections: 196 tests, 1,166 assertions, no failures, 22 documented skips.
197
+ - YARD: undocumented 0 (100%). RuboCop: 134 files, no offenses.
198
+ - Following explicit confirmation, the source license and gem metadata use Apache-2.0. No gem has
199
+ been published.
@@ -0,0 +1,17 @@
1
+ # NNNN: Decision title
2
+
3
+ - Status: Proposed
4
+ - Date: YYYY-MM-DD
5
+
6
+ ## Context
7
+
8
+ Describe the question, constraints, and forces that make the decision necessary.
9
+
10
+ ## Decision
11
+
12
+ Describe the chosen approach and the important alternatives that were not selected.
13
+
14
+ ## Consequences
15
+
16
+ Describe the positive and negative consequences, follow-up work, and conditions that could cause
17
+ this decision to be revisited.
@@ -0,0 +1,27 @@
1
+ # 0001: Pin the golden reference and define CPU-only validation
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-07-23
5
+
6
+ ## Context
7
+
8
+ Golden data must remain reproducible while the upstream `main` branch and its public tensor shapes
9
+ continue to evolve. The development host also has no CUDA device, so it cannot execute the complete
10
+ upstream rasterizer locally.
11
+
12
+ ## Decision
13
+
14
+ - Pin Python reference data to the latest official release, `gsplat==1.5.3` (tag commit
15
+ `937e29912570c372bed6747a5c9bf85fed877bae`), instead of a moving `main` branch.
16
+ - Use the upstream 1.5.3 elliptical radii shape `[C,N,2]` in projection and golden files. The
17
+ low-level intersection operation continues to accept legacy scalar radii.
18
+ - Do not create golden `last_ids`, because upstream 1.5.3 does not expose that internal rasterizer
19
+ value. Raster goldens store public outputs, all public input gradients, and `means2d.absgrad`.
20
+ - Use the dependency-free generator dry run as the local P0-4 gate. Keep CPU partial-generation and
21
+ the required CUDA command documented in `tools/README.md`.
22
+
23
+ ## Consequences
24
+
25
+ Golden inputs and public output contracts are reproducible against an immutable upstream version.
26
+ Local development can validate the generator and all CPU-owned behavior, while complete raster
27
+ parity remains an explicit external CUDA acceptance gate.
@@ -0,0 +1,26 @@
1
+ # 0002: Retain exact Ruby fallbacks for the native backend
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-07-23
5
+
6
+ ## Context
7
+
8
+ The native backend must accelerate production float32 training without creating a second,
9
+ divergent implementation of every derivative and compatibility path. Raster backward must also
10
+ remain within a practical memory bound at the 100k-Gaussian benchmark scale.
11
+
12
+ ## Decision
13
+
14
+ - Target contiguous `Numo::SFloat` arrays in native kernels.
15
+ - Keep float64 gradchecks, masked spherical harmonics, and analytic projection/SH backward on the
16
+ Ruby implementation. Selecting `:native` remains functionally complete through these fallbacks.
17
+ - Label performance results for these paths as hybrid and measure accelerated operations
18
+ explicitly.
19
+ - Use OpenMP atomic scatter-add in raster backward instead of per-worker full gradient buffers.
20
+
21
+ ## Consequences
22
+
23
+ The native backend accelerates the high-volume path while preserving one authoritative copy of
24
+ complex derivative formulas. Atomic scatter-add avoids `O(workers × N × channels)` temporary
25
+ memory. Its measured 100k/800×800 raster backward improves from 279.790 ms with one worker to
26
+ 79.252 ms with eight workers, at the cost of atomic-update contention.
@@ -0,0 +1,26 @@
1
+ # 0003: Share the Ruby projection for distorted cameras
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-07-23
5
+
6
+ ## Context
7
+
8
+ Equidistant fisheye and OpenCV distortion add calibration-sensitive formulas and numerical
9
+ derivatives. Independent Ruby and native implementations would increase the risk of camera-model
10
+ drift without improving the established pinhole performance path.
11
+
12
+ ## Decision
13
+
14
+ - Use one float32/float64 Ruby implementation of equidistant fisheye and OpenCV
15
+ radial/tangential/thin-prism projection for both backend selections.
16
+ - Delegate these camera models to the Ruby path when `:native` is selected.
17
+ - Follow gsplat 1.5.3's rational pinhole coefficient order: `k1..k3` form the numerator and
18
+ `k4..k6` form the denominator. Fisheye coefficients multiply `theta^3..theta^9`.
19
+ - Treat distortion coefficients as calibration constants. Propagate gradients to Gaussian
20
+ geometry, but do not optimize coefficients or intrinsics in this phase.
21
+
22
+ ## Consequences
23
+
24
+ Both backend selections have identical distorted-camera semantics and numerical derivatives. These
25
+ models run at reference speed; the optimized pinhole path and its analytic/native backward remain
26
+ unchanged.
@@ -0,0 +1,26 @@
1
+ # 0004: Use one portable reference for world-space extension paths
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-07-24
5
+
6
+ ## Context
7
+
8
+ Hit-distance rendering and local depth ordering require anisotropic Gaussian evaluation in world
9
+ space. They extend the renderer beyond the performance-oriented 2D EWA core and introduce geometry
10
+ derivatives that would be costly to duplicate across backends.
11
+
12
+ ## Decision
13
+
14
+ - Evaluate hit-distance modes against the anisotropic Gaussian in world space and replace the final
15
+ feature channel with the per-pixel closest-approach distance, matching the upstream eval3d
16
+ definition.
17
+ - Use the same Ruby implementation for both backend selections and a dtype-aware central-difference
18
+ geometry VJP. Keep the core 2D raster path analytic and native-accelerated.
19
+ - Make `global_z_order: false` sort projected centers by camera-space Euclidean distance. Continue
20
+ near/far culling by camera z and propagate the Euclidean-norm VJP to means.
21
+
22
+ ## Consequences
23
+
24
+ The extension remains differentiable and behaviorally identical under `:ruby` and `:native`
25
+ without maintaining two world-space evaluators. It prioritizes semantic coverage over large-scene
26
+ speed and leaves the established fast raster path untouched.
@@ -0,0 +1,24 @@
1
+ # 0005: Reuse the differentiable EWA core for 2DGS
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-07-24
5
+
6
+ ## Context
7
+
8
+ The Ruby API needs a portable 2DGS-compatible surface and Trainer mode, but an exact upstream
9
+ ray-splat transform and median-crossing compositor cannot be validated on this CPU-only host.
10
+
11
+ ## Decision
12
+
13
+ - Preserve the upstream seven-value return structure and metadata names.
14
+ - Reuse the established differentiable EWA footprint and compositor for color and alpha.
15
+ - Derive oriented camera normals, normalized surface normals, a depth-opacity distortion signal,
16
+ and expected depth as the median approximation.
17
+ - Treat exact ray-splat transforms, median crossing, and distortion accumulation as part of the
18
+ pending CUDA golden gate rather than claiming numerical identity.
19
+
20
+ ## Consequences
21
+
22
+ 2DGS color/alpha rendering and Trainer optimization remain differentiable on both backends with a
23
+ stable public shape. Auxiliary geometry buffers are approximate and must not be presented as an
24
+ exact substitute for upstream 2DGS until the external parity gate can be completed.
@@ -0,0 +1,25 @@
1
+ # 0006: Keep eval3d as a portable reference path
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-07-24
5
+
6
+ ## Context
7
+
8
+ `with_eval3d` generalizes the world-space evaluator from
9
+ [ADR 0004](0004-use-portable-world-space-reference-paths.md) to arbitrary color features, and
10
+ `return_normals` adds view-oriented Gaussian normals. These semantics are more important than
11
+ duplicating them in the native raster kernels before CUDA parity is available.
12
+
13
+ ## Decision
14
+
15
+ - Accumulate arbitrary features with the world-space evaluator.
16
+ - For normals, use the quaternion's canonical +Z axis, flip it toward the ray, and accumulate it
17
+ with the same alpha weights as color.
18
+ - Support pinhole cameras and classic rasterization.
19
+ - Share the Ruby evaluator and numerical geometry VJP between backend selections.
20
+
21
+ ## Consequences
22
+
23
+ The performance-oriented 2D raster kernels remain unchanged, while eval3d values and geometry stay
24
+ differentiable and portable. The analytic single-ray case validates values and quaternion
25
+ gradients locally; full CUDA color, alpha, and normal parity remains a generated-golden gate.
@@ -0,0 +1,25 @@
1
+ # 0007: Share compositor semantics for contribution indices
2
+
3
+ - Status: Accepted
4
+ - Date: 2026-07-24
5
+
6
+ ## Context
7
+
8
+ `rasterize_to_indices_in_range` exposes iterative per-pixel contribution analysis. Its range
9
+ coordinates, ordering, and termination rules must match the regular compositor or analysis results
10
+ will disagree with rendered pixels.
11
+
12
+ ## Decision
13
+
14
+ - Interpret range bounds as batches of `tile_size²` entries within each tile's sorted intersection
15
+ list, matching the upstream iterative rasterizer rather than indexing global intersections.
16
+ - Preserve image-major, pixel-major, then depth order.
17
+ - Apply the compositor's alpha skip, clamp, and exclusive transmittance stop.
18
+ - Use the portable Ruby enumerator for both backend selections and do not record an autograd graph
19
+ for integer analysis outputs.
20
+
21
+ ## Consequences
22
+
23
+ Contribution enumeration is deterministic and agrees with regular rasterization semantics. It runs
24
+ at reference speed under either backend and intentionally produces non-differentiable integer
25
+ indices.
@@ -0,0 +1,14 @@
1
+ # Example data
2
+
3
+ This directory contains a deterministic 16-Gaussian scene for the runnable examples:
4
+
5
+ - `colmap/` is a three-view COLMAP text dataset with 16×16 PNG images.
6
+ - `splats.ply` is the corresponding Inria-layout Gaussian model.
7
+
8
+ Regenerate both assets from the repository root:
9
+
10
+ ```bash
11
+ bundle exec ruby examples/generate_sample_data.rb
12
+ ```
13
+
14
+ The scene is generated entirely by Gsplat and contains no third-party data.
@@ -0,0 +1,2 @@
1
+ # One PINHOLE camera shared by all sample views.
2
+ 1 PINHOLE 16 16 16 16 8 8
@@ -0,0 +1,6 @@
1
+ # Each registered image is followed by an empty POINTS2D line.
2
+ 1 1 0 0 0 -0.120000 0 0 1 view_000.png
3
+
4
+ 2 1 0 0 0 0.000000 0 0 1 view_001.png
5
+
6
+ 3 1 0 0 0 0.120000 0 0 1 view_002.png
@@ -0,0 +1,17 @@
1
+ # Synthetic colored seed points.
2
+ 1 -0.875 -0.875 2 0 51 255 0
3
+ 2 -0.375 -0.875 2 17 60 238 0
4
+ 3 0.125 -0.875 2 34 68 221 0
5
+ 4 0.625 -0.875 2 51 77 204 0
6
+ 5 -0.875 -0.375 2 68 85 187 0
7
+ 6 -0.375 -0.375 2 85 94 170 0
8
+ 7 0.125 -0.375 2 102 102 153 0
9
+ 8 0.625 -0.375 2 119 111 136 0
10
+ 9 -0.875 0.125 2 136 119 119 0
11
+ 10 -0.375 0.125 2 153 128 102 0
12
+ 11 0.125 0.125 2 170 136 85 0
13
+ 12 0.625 0.125 2 187 144 68 0
14
+ 13 -0.875 0.625 2 204 153 51 0
15
+ 14 -0.375 0.625 2 221 161 34 0
16
+ 15 0.125 0.625 2 238 170 17 0
17
+ 16 0.625 0.625 2 255 178 0 0
Binary file