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.
- checksums.yaml +7 -0
- data/LICENSE.txt +202 -0
- data/README.md +236 -0
- data/docs/ACCEPTANCE.md +60 -0
- data/docs/BENCHMARKS.md +79 -0
- data/docs/DECISIONS.md +52 -0
- data/docs/MIGRATION.md +117 -0
- data/docs/PROFILE.md +75 -0
- data/docs/PROGRESS.md +199 -0
- data/docs/decisions/0000-template.md +17 -0
- data/docs/decisions/0001-pin-golden-reference-and-cpu-validation.md +27 -0
- data/docs/decisions/0002-retain-ruby-fallbacks-for-native-backend.md +26 -0
- data/docs/decisions/0003-share-projection-for-distorted-cameras.md +26 -0
- data/docs/decisions/0004-use-portable-world-space-reference-paths.md +26 -0
- data/docs/decisions/0005-reuse-ewa-core-for-2dgs.md +24 -0
- data/docs/decisions/0006-keep-eval3d-as-portable-reference.md +25 -0
- data/docs/decisions/0007-share-compositor-semantics-for-contribution-indices.md +25 -0
- data/examples/data/README.md +14 -0
- data/examples/data/colmap/images/view_000.png +0 -0
- data/examples/data/colmap/images/view_001.png +0 -0
- data/examples/data/colmap/images/view_002.png +0 -0
- data/examples/data/colmap/sparse/0/cameras.txt +2 -0
- data/examples/data/colmap/sparse/0/images.txt +6 -0
- data/examples/data/colmap/sparse/0/points3D.txt +17 -0
- data/examples/data/splats.ply +0 -0
- data/examples/fit_image.rb +35 -0
- data/examples/generate_sample_data.rb +147 -0
- data/examples/render_path.rb +117 -0
- data/examples/simple_trainer.rb +76 -0
- data/ext/gsplat_native/common.h +54 -0
- data/ext/gsplat_native/extconf.rb +39 -0
- data/ext/gsplat_native/gsplat_native.c +60 -0
- data/ext/gsplat_native/intersections.c +211 -0
- data/ext/gsplat_native/projection.c +199 -0
- data/ext/gsplat_native/raster_backward.c +129 -0
- data/ext/gsplat_native/raster_backward_bridge.c +69 -0
- data/ext/gsplat_native/raster_forward.c +150 -0
- data/ext/gsplat_native/rasterization.h +51 -0
- data/ext/gsplat_native/spherical_harmonics.c +129 -0
- data/gsplat.gemspec +36 -0
- data/lib/gsplat/autograd/context.rb +60 -0
- data/lib/gsplat/autograd/function.rb +68 -0
- data/lib/gsplat/autograd/variable.rb +159 -0
- data/lib/gsplat/backend/ruby/accumulate.rb +139 -0
- data/lib/gsplat/backend/ruby/accumulate_backward.rb +40 -0
- data/lib/gsplat/backend/ruby/eval3d_rasterizer.rb +175 -0
- data/lib/gsplat/backend/ruby/isect_tiles.rb +198 -0
- data/lib/gsplat/backend/ruby/projection.rb +251 -0
- data/lib/gsplat/backend/ruby/projection_backward.rb +190 -0
- data/lib/gsplat/backend/ruby/projection_covariance_vjp.rb +72 -0
- data/lib/gsplat/backend/ruby/projection_input_vjp.rb +252 -0
- data/lib/gsplat/backend/ruby/quat_scale_to_covar_preci.rb +139 -0
- data/lib/gsplat/backend/ruby/rasterize_to_indices_in_range.rb +121 -0
- data/lib/gsplat/backend/ruby/rasterize_to_pixels.rb +199 -0
- data/lib/gsplat/backend/ruby/rasterize_to_pixels_backward.rb +121 -0
- data/lib/gsplat/backend/ruby/spherical_harmonics.rb +135 -0
- data/lib/gsplat/backend/ruby/tile_compositor.rb +50 -0
- data/lib/gsplat/backend/ruby/tile_compositor_backward.rb +84 -0
- data/lib/gsplat/backend.rb +79 -0
- data/lib/gsplat/compression/grid_sort.rb +79 -0
- data/lib/gsplat/compression/kmeans.rb +121 -0
- data/lib/gsplat/compression/png.rb +147 -0
- data/lib/gsplat/compression/png_codec.rb +133 -0
- data/lib/gsplat/compression/quantizer.rb +79 -0
- data/lib/gsplat/io/checkpoint.rb +145 -0
- data/lib/gsplat/io/colmap.rb +175 -0
- data/lib/gsplat/io/colmap_binary.rb +98 -0
- data/lib/gsplat/io/colmap_text.rb +84 -0
- data/lib/gsplat/io/image.rb +63 -0
- data/lib/gsplat/io/image_backends.rb +81 -0
- data/lib/gsplat/io/npy.rb +189 -0
- data/lib/gsplat/io/ply.rb +185 -0
- data/lib/gsplat/io/ply_reader.rb +142 -0
- data/lib/gsplat/io/zip_archive.rb +183 -0
- data/lib/gsplat/math/camera_distortion.rb +123 -0
- data/lib/gsplat/math/camera_projection.rb +202 -0
- data/lib/gsplat/math/mat.rb +114 -0
- data/lib/gsplat/math/quaternion.rb +175 -0
- data/lib/gsplat/math/small_matrix_primitives.rb +112 -0
- data/lib/gsplat/math/spherical_harmonic_basis.rb +148 -0
- data/lib/gsplat/math/ssim.rb +153 -0
- data/lib/gsplat/native.rb +30 -0
- data/lib/gsplat/native_ops.rb +148 -0
- data/lib/gsplat/native_raster_ops.rb +76 -0
- data/lib/gsplat/ops/accumulate.rb +68 -0
- data/lib/gsplat/ops/eval3d_rasterize.rb +62 -0
- data/lib/gsplat/ops/isect_tiles.rb +38 -0
- data/lib/gsplat/ops/projection.rb +154 -0
- data/lib/gsplat/ops/quat_scale_to_covar_preci.rb +118 -0
- data/lib/gsplat/ops/rasterize_to_indices_in_range.rb +38 -0
- data/lib/gsplat/ops/rasterize_to_pixels.rb +105 -0
- data/lib/gsplat/ops/relocation.rb +94 -0
- data/lib/gsplat/ops/spherical_harmonics.rb +71 -0
- data/lib/gsplat/ops/tensor_shape_ops.rb +173 -0
- data/lib/gsplat/ops/tensor_value_ops.rb +150 -0
- data/lib/gsplat/optim/adam.rb +212 -0
- data/lib/gsplat/optim/lr_scheduler.rb +36 -0
- data/lib/gsplat/optim/selective_adam.rb +68 -0
- data/lib/gsplat/rasterization.rb +130 -0
- data/lib/gsplat/rasterization_2dgs.rb +140 -0
- data/lib/gsplat/rasterization_helpers.rb +162 -0
- data/lib/gsplat/rasterization_validation.rb +99 -0
- data/lib/gsplat/strategy/base.rb +49 -0
- data/lib/gsplat/strategy/default.rb +188 -0
- data/lib/gsplat/strategy/mcmc.rb +103 -0
- data/lib/gsplat/strategy/mcmc_ops.rb +143 -0
- data/lib/gsplat/strategy/ops.rb +165 -0
- data/lib/gsplat/training/config.rb +91 -0
- data/lib/gsplat/training/image_fitter.rb +158 -0
- data/lib/gsplat/training/losses.rb +191 -0
- data/lib/gsplat/training/scene.rb +116 -0
- data/lib/gsplat/training/trainer.rb +236 -0
- data/lib/gsplat/utils.rb +110 -0
- data/lib/gsplat/version.rb +6 -0
- data/lib/gsplat.rb +98 -0
- metadata +181 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gsplat
|
|
4
|
+
module Backend
|
|
5
|
+
# Tile-accelerated Numo rasterization forward pass.
|
|
6
|
+
module RubyRasterizeToPixels
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# rubocop:disable Metrics/ParameterLists
|
|
10
|
+
def forward(means2d, conics, colors, opacities, backgrounds, masks, width, height,
|
|
11
|
+
tile_size, isect_offsets, flatten_ids)
|
|
12
|
+
# rubocop:enable Metrics/ParameterLists
|
|
13
|
+
inputs = validate_inputs(
|
|
14
|
+
means2d, conics, colors, opacities, backgrounds, masks, width, height,
|
|
15
|
+
tile_size, isect_offsets, flatten_ids
|
|
16
|
+
)
|
|
17
|
+
means2d, conics, colors, opacities, backgrounds, masks, isect_offsets, flatten_ids = inputs
|
|
18
|
+
camera_count, gaussian_count = means2d.shape[0...2]
|
|
19
|
+
channel_count = colors.shape[-1]
|
|
20
|
+
tile_height, tile_width = isect_offsets.shape[-2..]
|
|
21
|
+
render_colors = means2d.class.zeros(camera_count, height, width, channel_count)
|
|
22
|
+
render_alphas = means2d.class.zeros(camera_count, height, width, 1)
|
|
23
|
+
last_ids = Numo::Int32.zeros(camera_count, height, width)
|
|
24
|
+
flattened_means = means2d.reshape(camera_count * gaussian_count, 2)
|
|
25
|
+
flattened_conics = conics.reshape(camera_count * gaussian_count, 3)
|
|
26
|
+
flattened_colors = colors.reshape(camera_count * gaussian_count, channel_count)
|
|
27
|
+
flattened_opacities = opacities.reshape(camera_count * gaussian_count)
|
|
28
|
+
camera_count.times do |camera_index|
|
|
29
|
+
tile_height.times do |tile_y|
|
|
30
|
+
tile_width.times do |tile_x|
|
|
31
|
+
render_tile!(
|
|
32
|
+
render_colors,
|
|
33
|
+
render_alphas,
|
|
34
|
+
last_ids,
|
|
35
|
+
flattened_means,
|
|
36
|
+
flattened_conics,
|
|
37
|
+
flattened_colors,
|
|
38
|
+
flattened_opacities,
|
|
39
|
+
backgrounds,
|
|
40
|
+
masks,
|
|
41
|
+
isect_offsets,
|
|
42
|
+
flatten_ids,
|
|
43
|
+
camera_index,
|
|
44
|
+
tile_x,
|
|
45
|
+
tile_y,
|
|
46
|
+
width,
|
|
47
|
+
height,
|
|
48
|
+
tile_size
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
[render_colors, render_alphas, last_ids]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# rubocop:disable Metrics/AbcSize, Metrics/ParameterLists
|
|
57
|
+
def validate_inputs(means2d, conics, colors, opacities, backgrounds, masks, width, height,
|
|
58
|
+
tile_size, isect_offsets, flatten_ids)
|
|
59
|
+
# rubocop:enable Metrics/AbcSize, Metrics/ParameterLists
|
|
60
|
+
unless means2d.is_a?(Numo::NArray) && [Numo::SFloat, Numo::DFloat].include?(means2d.class)
|
|
61
|
+
raise ArgumentError, "means2d must be Numo::SFloat or Numo::DFloat"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
valid_means = means2d.ndim == 3 && means2d.shape[-1] == 2
|
|
65
|
+
raise ShapeError, "expected means2d [C,N,2], got #{means2d.shape.inspect}" unless valid_means
|
|
66
|
+
|
|
67
|
+
leading_shape = means2d.shape[0...2]
|
|
68
|
+
conics = means2d.class.cast(conics)
|
|
69
|
+
colors = means2d.class.cast(colors)
|
|
70
|
+
opacities = means2d.class.cast(opacities)
|
|
71
|
+
valid = conics.shape == leading_shape + [3] &&
|
|
72
|
+
colors.ndim == 3 && colors.shape[0...2] == leading_shape &&
|
|
73
|
+
opacities.shape == leading_shape
|
|
74
|
+
unless valid
|
|
75
|
+
raise ShapeError,
|
|
76
|
+
"expected conics [C,N,3], colors [C,N,D], and opacities [C,N], " \
|
|
77
|
+
"got #{conics.shape.inspect}, #{colors.shape.inspect}, and #{opacities.shape.inspect}"
|
|
78
|
+
end
|
|
79
|
+
backgrounds = validate_backgrounds(means2d.class, backgrounds, means2d.shape[0], colors.shape[-1])
|
|
80
|
+
isect_offsets = Numo::Int32.cast(isect_offsets)
|
|
81
|
+
valid_offsets = isect_offsets.ndim == 3 && isect_offsets.shape[0] == means2d.shape[0]
|
|
82
|
+
raise ShapeError, "expected isect_offsets [C,TH,TW]" unless valid_offsets
|
|
83
|
+
|
|
84
|
+
masks = validate_masks(masks, isect_offsets.shape)
|
|
85
|
+
flatten_ids = Numo::Int32.cast(flatten_ids)
|
|
86
|
+
validate_indices!(flatten_ids, means2d.shape[0] * means2d.shape[1], isect_offsets)
|
|
87
|
+
validate_dimensions!(width, height, tile_size, isect_offsets.shape[-1], isect_offsets.shape[-2])
|
|
88
|
+
[means2d, conics, colors, opacities, backgrounds, masks, isect_offsets, flatten_ids]
|
|
89
|
+
end
|
|
90
|
+
private_class_method :validate_inputs
|
|
91
|
+
|
|
92
|
+
def validate_backgrounds(type, backgrounds, camera_count, channel_count)
|
|
93
|
+
return nil unless backgrounds
|
|
94
|
+
|
|
95
|
+
value = type.cast(backgrounds)
|
|
96
|
+
expected = [camera_count, channel_count]
|
|
97
|
+
unless value.shape == expected
|
|
98
|
+
raise ShapeError, "expected backgrounds #{expected.inspect}, got #{value.shape.inspect}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
value
|
|
102
|
+
end
|
|
103
|
+
private_class_method :validate_backgrounds
|
|
104
|
+
|
|
105
|
+
def validate_masks(masks, expected_shape)
|
|
106
|
+
return nil unless masks
|
|
107
|
+
|
|
108
|
+
value = Numo::Bit.cast(masks)
|
|
109
|
+
unless value.shape == expected_shape
|
|
110
|
+
raise ShapeError, "expected masks #{expected_shape.inspect}, got #{value.shape.inspect}"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
value
|
|
114
|
+
end
|
|
115
|
+
private_class_method :validate_masks
|
|
116
|
+
|
|
117
|
+
def validate_indices!(flatten_ids, gaussian_count, offsets)
|
|
118
|
+
raise ShapeError, "expected flatten_ids [M]" unless flatten_ids.ndim == 1
|
|
119
|
+
if flatten_ids.size.positive? && (flatten_ids.min.negative? || flatten_ids.max >= gaussian_count)
|
|
120
|
+
raise ShapeError, "flatten_ids contains an out-of-range Gaussian index"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
values = offsets.to_a.flatten
|
|
124
|
+
valid = values.each_cons(2).all? { |left, right| left <= right } &&
|
|
125
|
+
values.all? { |value| value.between?(0, flatten_ids.size) }
|
|
126
|
+
raise Gsplat::Error, "intersection offsets must be sorted indices into flatten_ids" unless valid
|
|
127
|
+
end
|
|
128
|
+
private_class_method :validate_indices!
|
|
129
|
+
|
|
130
|
+
def validate_dimensions!(width, height, tile_size, tile_width, tile_height)
|
|
131
|
+
valid = [width, height, tile_size].all? { |value| value.is_a?(Integer) && value.positive? }
|
|
132
|
+
raise ArgumentError, "image dimensions and tile_size must be positive integers" unless valid
|
|
133
|
+
return if (tile_width * tile_size) >= width && (tile_height * tile_size) >= height
|
|
134
|
+
|
|
135
|
+
raise ShapeError, "intersection offset grid does not cover the image"
|
|
136
|
+
end
|
|
137
|
+
private_class_method :validate_dimensions!
|
|
138
|
+
|
|
139
|
+
# rubocop:disable Metrics/AbcSize, Metrics/ParameterLists
|
|
140
|
+
def render_tile!(render_colors, render_alphas, last_ids, means2d, conics, colors, opacities,
|
|
141
|
+
backgrounds, masks, offsets, flatten_ids, camera_index, tile_x, tile_y,
|
|
142
|
+
width, height, tile_size)
|
|
143
|
+
# rubocop:enable Metrics/ParameterLists
|
|
144
|
+
x_start = tile_x * tile_size
|
|
145
|
+
y_start = tile_y * tile_size
|
|
146
|
+
x_end = [x_start + tile_size, width].min
|
|
147
|
+
y_end = [y_start + tile_size, height].min
|
|
148
|
+
return if x_start >= width || y_start >= height
|
|
149
|
+
|
|
150
|
+
if masks && masks[camera_index, tile_y, tile_x].zero?
|
|
151
|
+
fill_background!(render_colors, backgrounds, camera_index, x_start, x_end, y_start, y_end)
|
|
152
|
+
return
|
|
153
|
+
end
|
|
154
|
+
tile_width = offsets.shape[-1]
|
|
155
|
+
tile_height = offsets.shape[-2]
|
|
156
|
+
flat_tile = (((camera_index * tile_height) + tile_y) * tile_width) + tile_x
|
|
157
|
+
range_start = offsets[flat_tile]
|
|
158
|
+
range_end = flat_tile + 1 < offsets.size ? offsets[flat_tile + 1] : flatten_ids.size
|
|
159
|
+
pixels_x, pixels_y = tile_coordinates(means2d.class, x_start, x_end, y_start, y_end)
|
|
160
|
+
composited, transmittance, tile_last_ids = RubyTileCompositor.composite(
|
|
161
|
+
means2d, conics, colors, opacities, flatten_ids, range_start, range_end,
|
|
162
|
+
pixels_x, pixels_y
|
|
163
|
+
)
|
|
164
|
+
pixel_height = y_end - y_start
|
|
165
|
+
pixel_width = x_end - x_start
|
|
166
|
+
if backgrounds
|
|
167
|
+
composited += transmittance.reshape(transmittance.size, 1) *
|
|
168
|
+
backgrounds[camera_index, true].reshape(1, colors.shape[-1])
|
|
169
|
+
end
|
|
170
|
+
render_colors[camera_index, y_start...y_end, x_start...x_end, true] =
|
|
171
|
+
composited.reshape(pixel_height, pixel_width, colors.shape[-1])
|
|
172
|
+
render_alphas[camera_index, y_start...y_end, x_start...x_end, 0] =
|
|
173
|
+
(1 - transmittance).reshape(pixel_height, pixel_width)
|
|
174
|
+
last_ids[camera_index, y_start...y_end, x_start...x_end] =
|
|
175
|
+
tile_last_ids.reshape(pixel_height, pixel_width)
|
|
176
|
+
end
|
|
177
|
+
# rubocop:enable Metrics/AbcSize
|
|
178
|
+
private_class_method :render_tile!
|
|
179
|
+
|
|
180
|
+
# rubocop:disable Metrics/ParameterLists
|
|
181
|
+
def fill_background!(render_colors, backgrounds, camera_index, x_start, x_end, y_start, y_end)
|
|
182
|
+
# rubocop:enable Metrics/ParameterLists
|
|
183
|
+
return unless backgrounds
|
|
184
|
+
|
|
185
|
+
render_colors[camera_index, y_start...y_end, x_start...x_end, true] =
|
|
186
|
+
backgrounds[camera_index, true]
|
|
187
|
+
end
|
|
188
|
+
private_class_method :fill_background!
|
|
189
|
+
|
|
190
|
+
def tile_coordinates(type, x_start, x_end, y_start, y_end)
|
|
191
|
+
width = x_end - x_start
|
|
192
|
+
x_values = Array.new(y_end - y_start) { (x_start...x_end).map { |column| column + 0.5 } }.flatten
|
|
193
|
+
y_values = (y_start...y_end).flat_map { |row| Array.new(width, row + 0.5) }
|
|
194
|
+
[type.cast(x_values), type.cast(y_values)]
|
|
195
|
+
end
|
|
196
|
+
private_class_method :tile_coordinates
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gsplat
|
|
4
|
+
module Backend
|
|
5
|
+
# Tile orchestration for rasterization VJPs.
|
|
6
|
+
module RubyRasterizeToPixelsBackward
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# rubocop:disable Metrics/AbcSize, Metrics/ParameterLists
|
|
10
|
+
def backward(means2d, conics, colors, opacities, backgrounds, masks, width, height,
|
|
11
|
+
tile_size, isect_offsets, flatten_ids, render_alphas, last_ids,
|
|
12
|
+
grad_render_colors, grad_render_alphas, absgrad:)
|
|
13
|
+
# rubocop:enable Metrics/ParameterLists
|
|
14
|
+
type = means2d.class
|
|
15
|
+
color_gradient_shape = [means2d.shape[0], height, width, colors.shape[-1]]
|
|
16
|
+
grad_render_colors = validate_gradient(type, grad_render_colors, color_gradient_shape)
|
|
17
|
+
grad_render_alphas = validate_gradient(type, grad_render_alphas, [means2d.shape[0], height, width, 1])
|
|
18
|
+
camera_count, gaussian_count = means2d.shape[0...2]
|
|
19
|
+
total_gaussians = camera_count * gaussian_count
|
|
20
|
+
grad_means = type.zeros(total_gaussians, 2)
|
|
21
|
+
grad_conics = type.zeros(total_gaussians, 3)
|
|
22
|
+
grad_colors = type.zeros(total_gaussians, colors.shape[-1])
|
|
23
|
+
grad_opacities = type.zeros(total_gaussians)
|
|
24
|
+
grad_backgrounds = backgrounds && type.zeros(*backgrounds.shape)
|
|
25
|
+
grad_means_abs = absgrad ? type.zeros(total_gaussians, 2) : nil
|
|
26
|
+
flattened = flatten_inputs(means2d, conics, colors, opacities)
|
|
27
|
+
tile_height, tile_width = isect_offsets.shape[-2..]
|
|
28
|
+
camera_count.times do |camera_index|
|
|
29
|
+
tile_height.times do |tile_y|
|
|
30
|
+
tile_width.times do |tile_x|
|
|
31
|
+
backward_tile!(
|
|
32
|
+
grad_means, grad_conics, grad_colors, grad_opacities, grad_backgrounds,
|
|
33
|
+
*flattened, backgrounds, masks, isect_offsets, flatten_ids, render_alphas,
|
|
34
|
+
last_ids, grad_render_colors, grad_render_alphas, camera_index, tile_x, tile_y,
|
|
35
|
+
width, height, tile_size, grad_means_abs
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
gradients = [
|
|
41
|
+
grad_means.reshape(*means2d.shape),
|
|
42
|
+
grad_conics.reshape(*conics.shape),
|
|
43
|
+
grad_colors.reshape(*colors.shape),
|
|
44
|
+
grad_opacities.reshape(*opacities.shape),
|
|
45
|
+
grad_backgrounds
|
|
46
|
+
]
|
|
47
|
+
[gradients, grad_means_abs&.reshape(*means2d.shape)]
|
|
48
|
+
end
|
|
49
|
+
# rubocop:enable Metrics/AbcSize
|
|
50
|
+
|
|
51
|
+
def validate_gradient(type, gradient, expected_shape)
|
|
52
|
+
value = type.cast(gradient)
|
|
53
|
+
unless value.shape == expected_shape
|
|
54
|
+
raise ShapeError, "expected output gradient #{expected_shape.inspect}, got #{value.shape.inspect}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
value
|
|
58
|
+
end
|
|
59
|
+
private_class_method :validate_gradient
|
|
60
|
+
|
|
61
|
+
def flatten_inputs(means2d, conics, colors, opacities)
|
|
62
|
+
total = means2d.shape[0] * means2d.shape[1]
|
|
63
|
+
[
|
|
64
|
+
means2d.reshape(total, 2),
|
|
65
|
+
conics.reshape(total, 3),
|
|
66
|
+
colors.reshape(total, colors.shape[-1]),
|
|
67
|
+
opacities.reshape(total)
|
|
68
|
+
]
|
|
69
|
+
end
|
|
70
|
+
private_class_method :flatten_inputs
|
|
71
|
+
|
|
72
|
+
# rubocop:disable Metrics/AbcSize, Metrics/ParameterLists
|
|
73
|
+
def backward_tile!(grad_means, grad_conics, grad_colors, grad_opacities, grad_backgrounds,
|
|
74
|
+
means2d, conics, colors, opacities, backgrounds, masks, offsets,
|
|
75
|
+
flatten_ids, render_alphas, last_ids, grad_render_colors, grad_render_alphas,
|
|
76
|
+
camera_index, tile_x, tile_y, width, height, tile_size, grad_means_abs)
|
|
77
|
+
# rubocop:enable Metrics/ParameterLists
|
|
78
|
+
x_start = tile_x * tile_size
|
|
79
|
+
y_start = tile_y * tile_size
|
|
80
|
+
x_end = [x_start + tile_size, width].min
|
|
81
|
+
y_end = [y_start + tile_size, height].min
|
|
82
|
+
return if x_start >= width || y_start >= height
|
|
83
|
+
|
|
84
|
+
alpha_tile = render_alphas[camera_index, y_start...y_end, x_start...x_end, 0].flatten
|
|
85
|
+
color_gradient = grad_render_colors[camera_index, y_start...y_end, x_start...x_end, true]
|
|
86
|
+
color_gradient = color_gradient.reshape(alpha_tile.size, colors.shape[-1])
|
|
87
|
+
alpha_gradient = grad_render_alphas[camera_index, y_start...y_end, x_start...x_end, 0].flatten
|
|
88
|
+
last_id_tile = last_ids[camera_index, y_start...y_end, x_start...x_end].flatten
|
|
89
|
+
if grad_backgrounds
|
|
90
|
+
grad_backgrounds[camera_index, true] += (
|
|
91
|
+
color_gradient * (1 - alpha_tile).reshape(alpha_tile.size, 1)
|
|
92
|
+
).sum(axis: 0)
|
|
93
|
+
end
|
|
94
|
+
return if masks && masks[camera_index, tile_y, tile_x].zero?
|
|
95
|
+
|
|
96
|
+
tile_width = offsets.shape[-1]
|
|
97
|
+
tile_height = offsets.shape[-2]
|
|
98
|
+
flat_tile = (((camera_index * tile_height) + tile_y) * tile_width) + tile_x
|
|
99
|
+
range_start = offsets[flat_tile]
|
|
100
|
+
range_end = flat_tile + 1 < offsets.size ? offsets[flat_tile + 1] : flatten_ids.size
|
|
101
|
+
pixels_x, pixels_y = RubyRasterizeToPixels.send(
|
|
102
|
+
:tile_coordinates,
|
|
103
|
+
means2d.class,
|
|
104
|
+
x_start,
|
|
105
|
+
x_end,
|
|
106
|
+
y_start,
|
|
107
|
+
y_end
|
|
108
|
+
)
|
|
109
|
+
RubyTileCompositorBackward.backward!(
|
|
110
|
+
grad_means, grad_conics, grad_colors, grad_opacities, means2d, conics,
|
|
111
|
+
colors, opacities, flatten_ids, range_start, range_end, pixels_x, pixels_y,
|
|
112
|
+
alpha_tile, last_id_tile, color_gradient, alpha_gradient,
|
|
113
|
+
backgrounds && backgrounds[camera_index, true],
|
|
114
|
+
grad_means_abs: grad_means_abs
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
# rubocop:enable Metrics/AbcSize
|
|
118
|
+
private_class_method :backward_tile!
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gsplat
|
|
4
|
+
module Backend
|
|
5
|
+
# Numo forward/backward implementation of real spherical harmonics.
|
|
6
|
+
module RubySphericalHarmonics
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def forward(degree, directions, coefficients, masks: nil)
|
|
10
|
+
directions, coefficients, masks, leading_shape = validate_inputs(degree, directions, coefficients, masks)
|
|
11
|
+
count = directions.size / 3
|
|
12
|
+
basis_count = coefficients.shape[-2]
|
|
13
|
+
channel_count = coefficients.shape[-1]
|
|
14
|
+
normalized = normalize_directions(directions).reshape(count, 3)
|
|
15
|
+
bases, = Math::SphericalHarmonicBasis.evaluate(normalized, degree, basis_count)
|
|
16
|
+
apply_masks!(bases, nil, masks)
|
|
17
|
+
colors = (
|
|
18
|
+
bases.reshape(count, basis_count, 1) *
|
|
19
|
+
coefficients.reshape(count, basis_count, channel_count)
|
|
20
|
+
).sum(axis: 1)
|
|
21
|
+
colors.reshape(*(leading_shape + [channel_count]))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# rubocop:disable Metrics/ParameterLists
|
|
25
|
+
def backward(degree, directions, coefficients, grad_output, masks: nil, grad_dirs: true, grad_coeffs: true)
|
|
26
|
+
# rubocop:enable Metrics/ParameterLists
|
|
27
|
+
directions, coefficients, masks, leading_shape = validate_inputs(degree, directions, coefficients, masks)
|
|
28
|
+
count = directions.size / 3
|
|
29
|
+
basis_count = coefficients.shape[-2]
|
|
30
|
+
channel_count = coefficients.shape[-1]
|
|
31
|
+
gradient = directions.class.cast(grad_output)
|
|
32
|
+
expected = leading_shape + [channel_count]
|
|
33
|
+
unless gradient.shape == expected
|
|
34
|
+
raise ShapeError, "expected output gradient #{expected.inspect}, got #{gradient.shape.inspect}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
normalized = normalize_directions(directions).reshape(count, 3)
|
|
38
|
+
bases, derivatives = Math::SphericalHarmonicBasis.evaluate(normalized, degree, basis_count)
|
|
39
|
+
apply_masks!(bases, derivatives, masks)
|
|
40
|
+
gradient = gradient.reshape(count, channel_count)
|
|
41
|
+
coefficient_values = coefficients.reshape(count, basis_count, channel_count)
|
|
42
|
+
coefficient_gradient = if grad_coeffs
|
|
43
|
+
bases.reshape(count, basis_count, 1) * gradient.reshape(count, 1, channel_count)
|
|
44
|
+
end
|
|
45
|
+
if grad_dirs
|
|
46
|
+
direction_gradient = direction_gradient(
|
|
47
|
+
directions,
|
|
48
|
+
coefficient_values,
|
|
49
|
+
gradient,
|
|
50
|
+
derivatives,
|
|
51
|
+
leading_shape
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
[
|
|
55
|
+
direction_gradient,
|
|
56
|
+
coefficient_gradient&.reshape(*(leading_shape + [basis_count, channel_count]))
|
|
57
|
+
]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def validate_inputs(degree, directions, coefficients, masks)
|
|
61
|
+
raise ArgumentError, "degree must be an integer from 0 through 4" unless (0..4).cover?(degree)
|
|
62
|
+
|
|
63
|
+
validate_array_types!(directions, coefficients, masks)
|
|
64
|
+
leading_shape = directions.shape[0...-1]
|
|
65
|
+
valid_shapes = directions.ndim >= 1 && directions.shape[-1] == 3 &&
|
|
66
|
+
coefficients.ndim >= 2 &&
|
|
67
|
+
coefficients.shape[0...-2] == leading_shape &&
|
|
68
|
+
coefficients.shape[-2] >= ((degree + 1)**2)
|
|
69
|
+
unless valid_shapes
|
|
70
|
+
raise ShapeError,
|
|
71
|
+
"expected directions [...,3] and coefficients [...,K,D] with K >= #{(degree + 1)**2}, " \
|
|
72
|
+
"got #{directions.shape.inspect} and #{coefficients.shape.inspect}"
|
|
73
|
+
end
|
|
74
|
+
if masks && masks.shape != leading_shape
|
|
75
|
+
raise ShapeError, "expected masks #{leading_shape.inspect}, got #{masks.shape.inspect}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
[directions, directions.class.cast(coefficients), masks && Numo::Bit.cast(masks), leading_shape]
|
|
79
|
+
end
|
|
80
|
+
private_class_method :validate_inputs
|
|
81
|
+
|
|
82
|
+
def validate_array_types!(directions, coefficients, masks)
|
|
83
|
+
unless directions.is_a?(Numo::NArray) && [Numo::SFloat, Numo::DFloat].include?(directions.class)
|
|
84
|
+
raise ArgumentError, "directions must be Numo::SFloat or Numo::DFloat"
|
|
85
|
+
end
|
|
86
|
+
raise ArgumentError, "coefficients must be a Numo::NArray" unless coefficients.is_a?(Numo::NArray)
|
|
87
|
+
raise ArgumentError, "masks must be a Numo::NArray" if masks && !masks.is_a?(Numo::NArray)
|
|
88
|
+
end
|
|
89
|
+
private_class_method :validate_array_types!
|
|
90
|
+
|
|
91
|
+
def normalize_directions(directions)
|
|
92
|
+
flat = directions.reshape(directions.size / 3, 3)
|
|
93
|
+
norms = (flat**2).sum(axis: 1)**0.5
|
|
94
|
+
norms[norms.lt(1e-12)] = 1e-12
|
|
95
|
+
(flat / norms.reshape(flat.shape[0], 1)).reshape(*directions.shape)
|
|
96
|
+
end
|
|
97
|
+
private_class_method :normalize_directions
|
|
98
|
+
|
|
99
|
+
def apply_masks!(bases, derivatives, masks)
|
|
100
|
+
return unless masks
|
|
101
|
+
|
|
102
|
+
hidden = masks.reshape(masks.size).eq(0)
|
|
103
|
+
bases[hidden, true] = 0
|
|
104
|
+
derivatives[hidden, true, true] = 0 if derivatives
|
|
105
|
+
end
|
|
106
|
+
private_class_method :apply_masks!
|
|
107
|
+
|
|
108
|
+
def direction_gradient(directions, coefficients, gradient, derivatives, leading_shape)
|
|
109
|
+
count = directions.size / 3
|
|
110
|
+
basis_count = coefficients.shape[1]
|
|
111
|
+
grad_bases = (coefficients * gradient.reshape(count, 1, gradient.shape[1])).sum(axis: 2)
|
|
112
|
+
grad_normalized = (derivatives * grad_bases.reshape(count, basis_count, 1)).sum(axis: 1)
|
|
113
|
+
normalized_direction_vjp(
|
|
114
|
+
directions,
|
|
115
|
+
grad_normalized.reshape(*(leading_shape + [3]))
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
private_class_method :direction_gradient
|
|
119
|
+
|
|
120
|
+
def normalized_direction_vjp(directions, gradient)
|
|
121
|
+
flat = directions.reshape(directions.size / 3, 3)
|
|
122
|
+
grad_flat = gradient.reshape(flat.shape[0], 3)
|
|
123
|
+
norms = (flat**2).sum(axis: 1)**0.5
|
|
124
|
+
clamped = norms.lt(1e-12)
|
|
125
|
+
norms[clamped] = 1e-12
|
|
126
|
+
normalized = flat / norms.reshape(flat.shape[0], 1)
|
|
127
|
+
dot = (normalized * grad_flat).sum(axis: 1)
|
|
128
|
+
result = (grad_flat - (normalized * dot.reshape(flat.shape[0], 1))) / norms.reshape(flat.shape[0], 1)
|
|
129
|
+
result[clamped, true] = grad_flat[clamped, true] / 1e-12 if clamped.any?
|
|
130
|
+
result.reshape(*directions.shape)
|
|
131
|
+
end
|
|
132
|
+
private_class_method :normalized_direction_vjp
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gsplat
|
|
4
|
+
module Backend
|
|
5
|
+
# Vectorized per-tile alpha compositing kernel.
|
|
6
|
+
module RubyTileCompositor
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# rubocop:disable Metrics/AbcSize, Metrics/ParameterLists
|
|
10
|
+
def composite(means2d, conics, colors, opacities, flatten_ids, range_start, range_end,
|
|
11
|
+
pixels_x, pixels_y)
|
|
12
|
+
# rubocop:enable Metrics/ParameterLists
|
|
13
|
+
pixel_count = pixels_x.size
|
|
14
|
+
channel_count = colors.shape[-1]
|
|
15
|
+
transmittance = means2d.class.ones(pixel_count)
|
|
16
|
+
composited = means2d.class.zeros(pixel_count, channel_count)
|
|
17
|
+
last_ids = Numo::Int32.zeros(pixel_count)
|
|
18
|
+
active = Numo::Bit.ones(pixel_count)
|
|
19
|
+
(range_start...range_end).each do |intersection_index|
|
|
20
|
+
gaussian_index = flatten_ids[intersection_index]
|
|
21
|
+
delta_x = means2d[gaussian_index, 0] - pixels_x
|
|
22
|
+
delta_y = means2d[gaussian_index, 1] - pixels_y
|
|
23
|
+
sigma = (0.5 * (
|
|
24
|
+
(conics[gaussian_index, 0] * (delta_x**2)) +
|
|
25
|
+
(conics[gaussian_index, 2] * (delta_y**2))
|
|
26
|
+
)) + (conics[gaussian_index, 1] * delta_x * delta_y)
|
|
27
|
+
alpha = opacities[gaussian_index] * Numo::NMath.exp(-sigma)
|
|
28
|
+
above_clamp = alpha.gt(RubyAccumulate::ALPHA_CLAMP)
|
|
29
|
+
alpha[above_clamp] = RubyAccumulate::ALPHA_CLAMP if above_clamp.any?
|
|
30
|
+
candidate = active & sigma.ge(0) & alpha.ge(RubyAccumulate::ALPHA_SKIP)
|
|
31
|
+
next_transmittance = transmittance * (1 - alpha)
|
|
32
|
+
accepted = candidate & next_transmittance.gt(RubyAccumulate::TRANSMITTANCE_STOP)
|
|
33
|
+
if accepted.any?
|
|
34
|
+
contribution = means2d.class.zeros(pixel_count)
|
|
35
|
+
contribution[accepted] = (alpha * transmittance)[accepted]
|
|
36
|
+
composited += contribution.reshape(pixel_count, 1) *
|
|
37
|
+
colors[gaussian_index, true].reshape(1, channel_count)
|
|
38
|
+
transmittance[accepted] = next_transmittance[accepted]
|
|
39
|
+
last_ids[accepted] = intersection_index
|
|
40
|
+
end
|
|
41
|
+
terminating = candidate & next_transmittance.le(RubyAccumulate::TRANSMITTANCE_STOP)
|
|
42
|
+
active[terminating] = 0 if terminating.any?
|
|
43
|
+
break unless active.any?
|
|
44
|
+
end
|
|
45
|
+
[composited, transmittance, last_ids]
|
|
46
|
+
end
|
|
47
|
+
# rubocop:enable Metrics/AbcSize
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gsplat
|
|
4
|
+
module Backend
|
|
5
|
+
# Reverse per-tile compositor with scatter-add into Gaussian gradients.
|
|
6
|
+
module RubyTileCompositorBackward
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# rubocop:disable Metrics/AbcSize, Metrics/BlockLength, Metrics/MethodLength, Metrics/ParameterLists
|
|
10
|
+
def backward!(grad_means, grad_conics, grad_colors, grad_opacities, means2d, conics,
|
|
11
|
+
colors, opacities, flatten_ids, range_start, range_end, pixels_x, pixels_y,
|
|
12
|
+
render_alphas, last_ids, grad_render_colors, grad_render_alphas, background,
|
|
13
|
+
grad_means_abs: nil)
|
|
14
|
+
# rubocop:enable Metrics/ParameterLists
|
|
15
|
+
pixel_count = pixels_x.size
|
|
16
|
+
current_transmittance = 1 - render_alphas
|
|
17
|
+
after_transmittance = means2d.class.ones(pixel_count)
|
|
18
|
+
remaining_color = means2d.class.zeros(pixel_count, colors.shape[-1])
|
|
19
|
+
remaining_color[true, true] = background if background
|
|
20
|
+
(range_start...range_end).reverse_each do |intersection_index|
|
|
21
|
+
gaussian_index = flatten_ids[intersection_index]
|
|
22
|
+
delta_x = means2d[gaussian_index, 0] - pixels_x
|
|
23
|
+
delta_y = means2d[gaussian_index, 1] - pixels_y
|
|
24
|
+
sigma = (0.5 * (
|
|
25
|
+
(conics[gaussian_index, 0] * (delta_x**2)) +
|
|
26
|
+
(conics[gaussian_index, 2] * (delta_y**2))
|
|
27
|
+
)) + (conics[gaussian_index, 1] * delta_x * delta_y)
|
|
28
|
+
gaussian_response = Numo::NMath.exp(-sigma)
|
|
29
|
+
raw_alpha = opacities[gaussian_index] * gaussian_response
|
|
30
|
+
alpha = raw_alpha.dup
|
|
31
|
+
clamped = alpha.gt(RubyAccumulate::ALPHA_CLAMP)
|
|
32
|
+
alpha[clamped] = RubyAccumulate::ALPHA_CLAMP if clamped.any?
|
|
33
|
+
valid = render_alphas.gt(0) & sigma.ge(0) & alpha.ge(RubyAccumulate::ALPHA_SKIP)
|
|
34
|
+
valid &= last_ids.ge(intersection_index)
|
|
35
|
+
valid &= current_transmittance.lt(1)
|
|
36
|
+
valid &= alpha.lt(1)
|
|
37
|
+
next unless valid.any?
|
|
38
|
+
|
|
39
|
+
transmittance_before = current_transmittance.dup
|
|
40
|
+
transmittance_before[valid] = current_transmittance[valid] / (1 - alpha[valid])
|
|
41
|
+
visibility = means2d.class.zeros(pixel_count)
|
|
42
|
+
visibility[valid] = (transmittance_before * alpha)[valid]
|
|
43
|
+
color = colors[gaussian_index, true]
|
|
44
|
+
grad_colors[gaussian_index, true] += (
|
|
45
|
+
grad_render_colors * visibility.reshape(pixel_count, 1)
|
|
46
|
+
).sum(axis: 0)
|
|
47
|
+
color_difference = color.reshape(1, colors.shape[-1]) - remaining_color
|
|
48
|
+
grad_alpha = transmittance_before * (
|
|
49
|
+
(grad_render_colors * color_difference).sum(axis: 1) +
|
|
50
|
+
(grad_render_alphas * after_transmittance)
|
|
51
|
+
)
|
|
52
|
+
unclamped = valid & raw_alpha.lt(RubyAccumulate::ALPHA_CLAMP)
|
|
53
|
+
grad_raw_alpha = means2d.class.zeros(pixel_count)
|
|
54
|
+
grad_raw_alpha[unclamped] = grad_alpha[unclamped] if unclamped.any?
|
|
55
|
+
grad_opacities[gaussian_index] += (grad_raw_alpha * gaussian_response).sum
|
|
56
|
+
grad_sigma = -grad_raw_alpha * raw_alpha
|
|
57
|
+
mean_x = grad_sigma * (
|
|
58
|
+
(conics[gaussian_index, 0] * delta_x) +
|
|
59
|
+
(conics[gaussian_index, 1] * delta_y)
|
|
60
|
+
)
|
|
61
|
+
mean_y = grad_sigma * (
|
|
62
|
+
(conics[gaussian_index, 2] * delta_y) +
|
|
63
|
+
(conics[gaussian_index, 1] * delta_x)
|
|
64
|
+
)
|
|
65
|
+
grad_means[gaussian_index, 0] += mean_x.sum
|
|
66
|
+
grad_means[gaussian_index, 1] += mean_y.sum
|
|
67
|
+
if grad_means_abs
|
|
68
|
+
grad_means_abs[gaussian_index, 0] += mean_x.abs.sum
|
|
69
|
+
grad_means_abs[gaussian_index, 1] += mean_y.abs.sum
|
|
70
|
+
end
|
|
71
|
+
grad_conics[gaussian_index, 0] += (grad_sigma * 0.5 * (delta_x**2)).sum
|
|
72
|
+
grad_conics[gaussian_index, 1] += (grad_sigma * delta_x * delta_y).sum
|
|
73
|
+
grad_conics[gaussian_index, 2] += (grad_sigma * 0.5 * (delta_y**2)).sum
|
|
74
|
+
updated_color = (alpha.reshape(pixel_count, 1) * color.reshape(1, colors.shape[-1])) +
|
|
75
|
+
((1 - alpha).reshape(pixel_count, 1) * remaining_color)
|
|
76
|
+
remaining_color[valid, true] = updated_color[valid, true]
|
|
77
|
+
after_transmittance[valid] *= 1 - alpha[valid]
|
|
78
|
+
current_transmittance[valid] = transmittance_before[valid]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
# rubocop:enable Metrics/AbcSize, Metrics/BlockLength, Metrics/MethodLength
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gsplat
|
|
4
|
+
# Operation registry and runtime backend selector.
|
|
5
|
+
module Backend
|
|
6
|
+
# Backend names accepted by {Gsplat.backend=}.
|
|
7
|
+
VALID_BACKENDS = %i[auto ruby native].freeze
|
|
8
|
+
|
|
9
|
+
@registry = Hash.new { |operations, name| operations[name] = {} }
|
|
10
|
+
@mutex = Mutex.new
|
|
11
|
+
@fallback_warned = false
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
# Registers an operation implementation.
|
|
15
|
+
#
|
|
16
|
+
# @param op_name [Symbol, String] operation identifier
|
|
17
|
+
# @param backend [Symbol, String] implementation backend (:ruby or :native)
|
|
18
|
+
# @param callable [#call, nil] implementation callable
|
|
19
|
+
# @yield implementation body when callable is omitted
|
|
20
|
+
# @return [#call] the registered implementation
|
|
21
|
+
def register(op_name, backend, callable = nil, &block)
|
|
22
|
+
implementation = callable || block
|
|
23
|
+
raise ArgumentError, "backend implementation must respond to #call" unless implementation.respond_to?(:call)
|
|
24
|
+
|
|
25
|
+
backend = normalize_backend(backend, allow_auto: false)
|
|
26
|
+
@mutex.synchronize { @registry[op_name.to_sym][backend] = implementation }
|
|
27
|
+
implementation
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Dispatches an operation to the selected implementation.
|
|
31
|
+
#
|
|
32
|
+
# @param op_name [Symbol, String] operation identifier
|
|
33
|
+
# @return [Object] operation result
|
|
34
|
+
def dispatch(op_name, ...)
|
|
35
|
+
operation = op_name.to_sym
|
|
36
|
+
implementation = implementation_for(operation)
|
|
37
|
+
implementation.call(...)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Validates and normalizes a backend name.
|
|
41
|
+
#
|
|
42
|
+
# @param backend [Symbol, String]
|
|
43
|
+
# @param allow_auto [Boolean]
|
|
44
|
+
# @return [Symbol]
|
|
45
|
+
def normalize_backend(backend, allow_auto: true)
|
|
46
|
+
normalized = backend.to_s.downcase.to_sym
|
|
47
|
+
valid = allow_auto ? VALID_BACKENDS : VALID_BACKENDS.drop(1)
|
|
48
|
+
return normalized if valid.include?(normalized)
|
|
49
|
+
|
|
50
|
+
raise ArgumentError, "unknown backend #{backend.inspect}; expected one of #{valid.join(', ')}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def implementation_for(operation)
|
|
56
|
+
implementations = @mutex.synchronize { @registry.fetch(operation, {}).dup }
|
|
57
|
+
selected = Gsplat.backend
|
|
58
|
+
return implementations.fetch(selected) { missing_backend!(operation, selected) } unless selected == :auto
|
|
59
|
+
return implementations[:native] if implementations.key?(:native)
|
|
60
|
+
|
|
61
|
+
warn_fallback_once
|
|
62
|
+
implementations.fetch(:ruby) { missing_backend!(operation, :ruby) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def missing_backend!(operation, backend)
|
|
66
|
+
raise NotSupportedError, "#{backend} backend does not implement #{operation}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def warn_fallback_once
|
|
70
|
+
should_warn = @mutex.synchronize do
|
|
71
|
+
next false if @fallback_warned
|
|
72
|
+
|
|
73
|
+
@fallback_warned = true
|
|
74
|
+
end
|
|
75
|
+
Gsplat.logger.warn("native backend is unavailable; falling back to ruby") if should_warn
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|