wgpu 1.1.0 → 1.2.1

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +88 -0
  3. data/README.md +44 -5
  4. data/docs/README.md +22 -0
  5. data/docs/api_coverage.md +140 -0
  6. data/docs/async.md +41 -0
  7. data/docs/bind_groups.md +25 -0
  8. data/docs/buffer_data.md +37 -0
  9. data/docs/command_encoding.md +49 -0
  10. data/docs/errors.md +40 -0
  11. data/docs/getting_started_compute.md +62 -0
  12. data/docs/getting_started_rendering.md +62 -0
  13. data/docs/installation.md +94 -0
  14. data/docs/pipeline_descriptors.md +43 -0
  15. data/docs/releasing.md +32 -0
  16. data/docs/resource_lifetime.md +108 -0
  17. data/docs/shaders.md +32 -0
  18. data/docs/texture_readback.md +46 -0
  19. data/docs/troubleshooting.md +53 -0
  20. data/docs/upgrading_wgpu_native.md +37 -0
  21. data/ext/wgpu/extconf.rb +10 -142
  22. data/lib/wgpu/async_task.rb +19 -0
  23. data/lib/wgpu/commands/command_buffer.rb +25 -1
  24. data/lib/wgpu/commands/command_encoder.rb +123 -9
  25. data/lib/wgpu/commands/compute_pass.rb +53 -0
  26. data/lib/wgpu/commands/render_bundle.rb +9 -1
  27. data/lib/wgpu/commands/render_bundle_encoder.rb +65 -4
  28. data/lib/wgpu/commands/render_pass.rb +136 -8
  29. data/lib/wgpu/core/adapter.rb +123 -19
  30. data/lib/wgpu/core/async_waiter.rb +47 -4
  31. data/lib/wgpu/core/canvas_context.rb +32 -2
  32. data/lib/wgpu/core/device.rb +399 -53
  33. data/lib/wgpu/core/instance.rb +28 -4
  34. data/lib/wgpu/core/queue.rb +197 -51
  35. data/lib/wgpu/core/surface.rb +64 -17
  36. data/lib/wgpu/data_types.rb +83 -0
  37. data/lib/wgpu/descriptor_helpers.rb +104 -0
  38. data/lib/wgpu/error.rb +70 -0
  39. data/lib/wgpu/logging.rb +63 -0
  40. data/lib/wgpu/native/abi_verifier.rb +143 -0
  41. data/lib/wgpu/native/callbacks.rb +10 -1
  42. data/lib/wgpu/native/capabilities.rb +32 -2
  43. data/lib/wgpu/native/distribution.rb +176 -0
  44. data/lib/wgpu/native/enum_helper.rb +80 -0
  45. data/lib/wgpu/native/enums.rb +68 -8
  46. data/lib/wgpu/native/fixtures/webgpu-v27.0.4.0-enums.h +848 -0
  47. data/lib/wgpu/native/functions.rb +21 -10
  48. data/lib/wgpu/native/installer.rb +223 -0
  49. data/lib/wgpu/native/loader.rb +61 -21
  50. data/lib/wgpu/native/structs.rb +18 -1
  51. data/lib/wgpu/native_resource.rb +342 -0
  52. data/lib/wgpu/pipeline/bind_group.rb +21 -0
  53. data/lib/wgpu/pipeline/bind_group_layout.rb +108 -41
  54. data/lib/wgpu/pipeline/compute_pipeline.rb +40 -50
  55. data/lib/wgpu/pipeline/pipeline_layout.rb +8 -0
  56. data/lib/wgpu/pipeline/render_pipeline.rb +207 -110
  57. data/lib/wgpu/pipeline/shader_module.rb +95 -28
  58. data/lib/wgpu/resources/buffer.rb +387 -85
  59. data/lib/wgpu/resources/query_set.rb +26 -12
  60. data/lib/wgpu/resources/sampler.rb +43 -20
  61. data/lib/wgpu/resources/texture.rb +89 -48
  62. data/lib/wgpu/resources/texture_view.rb +35 -7
  63. data/lib/wgpu/texture_format.rb +96 -0
  64. data/lib/wgpu/version.rb +1 -1
  65. data/lib/wgpu/window.rb +34 -1
  66. data/lib/wgpu.rb +32 -0
  67. data/sig/wgpu.rbs +460 -0
  68. metadata +33 -16
@@ -0,0 +1,342 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WGPU
4
+ class << self
5
+ attr_accessor :debug_leaks
6
+ end
7
+
8
+ self.debug_leaks = ENV["WGPU_DEBUG_LEAKS"] == "1"
9
+
10
+ module LeakTracker
11
+ @resources = {}
12
+ @mutex = Mutex.new
13
+
14
+ module_function
15
+
16
+ # Registers a live native resource when leak debugging is enabled.
17
+ #
18
+ # @param resource [NativeResource] resource to track
19
+ # @return [void]
20
+ def register(resource)
21
+ return unless WGPU.debug_leaks
22
+
23
+ object_id = resource.object_id
24
+ description = describe(resource)
25
+ @mutex.synchronize { @resources[object_id] = description }
26
+ ObjectSpace.define_finalizer(resource, finalizer(object_id))
27
+ end
28
+
29
+ # Removes a resource from leak tracking.
30
+ #
31
+ # @param resource [NativeResource] resource that was released
32
+ # @return [void]
33
+ def unregister(resource)
34
+ @mutex.synchronize { @resources.delete(resource.object_id) }
35
+ end
36
+
37
+ # Warns about and clears every tracked resource.
38
+ #
39
+ # @return [void]
40
+ def warn_remaining
41
+ resources = @mutex.synchronize do
42
+ remaining = @resources.values
43
+ @resources.clear
44
+ remaining
45
+ end
46
+ resources.each { |description| warn "WGPU resource leaked: #{description}" }
47
+ end
48
+
49
+ def describe(resource)
50
+ label = resource.label
51
+ label_text = label ? " label=#{label.inspect}" : ""
52
+ "#{resource.class}#{label_text}"
53
+ end
54
+ private_class_method :describe
55
+
56
+ def finalizer(object_id)
57
+ proc do
58
+ description = @mutex.synchronize { @resources.delete(object_id) }
59
+ warn "WGPU resource leaked: #{description}" if description
60
+ end
61
+ end
62
+ private_class_method :finalizer
63
+ end
64
+
65
+ at_exit { LeakTracker.warn_remaining if WGPU.debug_leaks }
66
+
67
+ class DeviceCallbackLifetime
68
+ # Creates a shared lifetime that runs cleanup after its final owner releases.
69
+ # @yield cleanup invoked exactly once
70
+ def initialize(&cleanup)
71
+ @cleanup = cleanup
72
+ @references = 0
73
+ @completed = false
74
+ @mutex = Mutex.new
75
+ end
76
+
77
+ # Adds an owning native wrapper.
78
+ # @return [void]
79
+ def retain
80
+ @mutex.synchronize do
81
+ raise ResourceError, "device callback lifetime is already complete" if @completed
82
+
83
+ @references += 1
84
+ end
85
+ end
86
+
87
+ # Removes an owning native wrapper and cleans up after the final owner.
88
+ # @return [void]
89
+ def release
90
+ cleanup = @mutex.synchronize do
91
+ return if @completed
92
+
93
+ @references -= 1
94
+ raise ResourceError, "device callback lifetime reference underflow" if @references.negative?
95
+ next unless @references.zero?
96
+
97
+ @completed = true
98
+ @cleanup
99
+ end
100
+ cleanup&.call
101
+ end
102
+ end
103
+ private_constant :DeviceCallbackLifetime
104
+
105
+ module CallbackKeepalive
106
+ INITIALIZATION_MUTEX = Mutex.new
107
+ RETAINED_MUTEX = Mutex.new
108
+ RETAINED_CALLBACKS = {}
109
+
110
+ module_function
111
+
112
+ # Retains an FFI callback for as long as an operation needs it.
113
+ #
114
+ # @param owner [Object] object that owns the callback registry
115
+ # @param callback [FFI::Function] callback to retain
116
+ # @return [Object] opaque token used by {.release}
117
+ def retain(owner, callback)
118
+ mutex, callbacks = storage_for(owner)
119
+ token = Object.new
120
+ mutex.synchronize { callbacks[token] = callback }
121
+ RETAINED_MUTEX.synchronize { RETAINED_CALLBACKS[token] = callback }
122
+ token
123
+ end
124
+
125
+ # Releases a retained callback token.
126
+ #
127
+ # @param owner [Object] object that owns the callback registry
128
+ # @param token [Object, nil] token returned by {.retain}
129
+ # @return [void]
130
+ def release(owner, token)
131
+ return unless token
132
+
133
+ mutex, callbacks = storage_for(owner)
134
+ mutex.synchronize { callbacks.delete(token) }
135
+ RETAINED_MUTEX.synchronize { RETAINED_CALLBACKS.delete(token) }
136
+ end
137
+
138
+ # Moves a retained callback token to another owner without unrooting it.
139
+ #
140
+ # @param from [Object] current registry owner
141
+ # @param to [Object] new registry owner
142
+ # @param token [Object] token returned by {.retain}
143
+ # @return [Boolean] whether the live token was transferred
144
+ def transfer(from, to, token)
145
+ return false unless token
146
+
147
+ callback = RETAINED_MUTEX.synchronize { RETAINED_CALLBACKS[token] }
148
+ return false unless callback
149
+
150
+ from_mutex, from_callbacks = storage_for(from)
151
+ to_mutex, to_callbacks = storage_for(to)
152
+ from_mutex.synchronize { from_callbacks.delete(token) }
153
+ to_mutex.synchronize { to_callbacks[token] = callback }
154
+ true
155
+ end
156
+
157
+ # Returns the number of callbacks retained for an owner.
158
+ #
159
+ # @param owner [Object] object that owns the callback registry
160
+ # @return [Integer] retained callback count
161
+ def count(owner)
162
+ mutex, callbacks = storage_for(owner)
163
+ mutex.synchronize { callbacks.length }
164
+ end
165
+
166
+ def storage_for(owner)
167
+ INITIALIZATION_MUTEX.synchronize do
168
+ mutex = owner.instance_variable_get(:@wgpu_callback_keepalive_mutex)
169
+ callbacks = owner.instance_variable_get(:@wgpu_callback_keepalive)
170
+ unless mutex && callbacks
171
+ mutex = Mutex.new
172
+ callbacks = {}
173
+ owner.instance_variable_set(:@wgpu_callback_keepalive_mutex, mutex)
174
+ owner.instance_variable_set(:@wgpu_callback_keepalive, callbacks)
175
+ end
176
+ [mutex, callbacks]
177
+ end
178
+ end
179
+ private_class_method :storage_for
180
+ end
181
+
182
+ module NativeResource
183
+ GUARDED_METHOD_EXEMPTIONS = [:initialize, :release, :released?, :handle, :label, :inspect].freeze
184
+
185
+ module ClassMethods
186
+ private
187
+
188
+ def adopt_native_handle(handle, label: nil)
189
+ resource = allocate
190
+ resource.send(:initialize_native_resource, handle: handle, label: label)
191
+ end
192
+ end
193
+
194
+ module Lifecycle
195
+ UNSET = Object.new.freeze
196
+ private_constant :UNSET
197
+
198
+ # Initializes a wrapper and registers its native-resource lifecycle.
199
+ #
200
+ # @param args [Array] positional arguments forwarded to the resource
201
+ # @param kwargs [Hash] keyword arguments forwarded to the resource
202
+ # @yield block forwarded to the resource initializer
203
+ # @return [NativeResource] initialized resource
204
+ def initialize(*args, **kwargs, &block)
205
+ super
206
+ initialize_native_resource(label: kwargs.fetch(:label, UNSET))
207
+ attach_device_callback_lifetime_from_parent
208
+ end
209
+
210
+ # Releases a resource once and unregisters it from leak tracking.
211
+ #
212
+ # @return [void]
213
+ def release(...)
214
+ return if released?
215
+
216
+ result = super
217
+ release_device_callback_lifetime
218
+ @released = true
219
+ LeakTracker.unregister(self)
220
+ result
221
+ end
222
+
223
+ private
224
+
225
+ def initialize_native_resource(handle: UNSET, label: UNSET)
226
+ @handle = handle unless handle.equal?(UNSET)
227
+ @label = label unless label.equal?(UNSET)
228
+ @released = false
229
+ LeakTracker.register(self)
230
+ self
231
+ end
232
+ end
233
+
234
+ def self.included(base)
235
+ guard = Module.new
236
+ base.public_instance_methods(false).each do |method_name|
237
+ next if GUARDED_METHOD_EXEMPTIONS.include?(method_name)
238
+
239
+ guard.define_method(method_name) do |*args, **kwargs, &block|
240
+ ensure_not_released!
241
+ super(*args, **kwargs, &block)
242
+ end
243
+ end
244
+ base.extend(ClassMethods)
245
+ base.prepend(guard)
246
+ base.prepend(Lifecycle)
247
+ end
248
+
249
+ attr_reader :label
250
+
251
+ # Reports whether the native handle has been released or is null.
252
+ # @return [Boolean]
253
+ def released?
254
+ return true if @released
255
+ return false unless instance_variable_defined?(:@handle)
256
+ return true if @handle.nil?
257
+
258
+ @handle.respond_to?(:null?) && @handle.null?
259
+ end
260
+
261
+ # Yields this wrapper and always releases it when the block exits.
262
+ #
263
+ # This is Ruby convenience API; WebGPU itself has no block-scoped resource
264
+ # primitive.
265
+ #
266
+ # @yieldparam resource [NativeResource] this resource
267
+ # @return the block result
268
+ def use
269
+ raise ArgumentError, "block is required" unless block_given?
270
+
271
+ ensure_not_released!
272
+ yield self
273
+ ensure
274
+ release if block_given? && !released?
275
+ end
276
+
277
+ # Returns a concise lifecycle-oriented representation.
278
+ #
279
+ # @return [String] class, optional label, and release state
280
+ def inspect
281
+ label_text = @label ? " label=#{@label.inspect}" : ""
282
+ "#<#{self.class}#{label_text} released=#{released?}>"
283
+ end
284
+
285
+ private
286
+
287
+ def attach_device_callback_lifetime(owner)
288
+ lifetime = owner&.instance_variable_get(:@device_callback_lifetime)
289
+ return self unless lifetime
290
+
291
+ lifetime.retain
292
+ @device_callback_lifetime = lifetime
293
+ @device_callback_lifetime_retained = true
294
+ self
295
+ end
296
+
297
+ def attach_device_callback_lifetime_from_parent
298
+ return if @device_callback_lifetime_retained
299
+
300
+ if @device_callback_lifetime
301
+ @device_callback_lifetime.retain
302
+ @device_callback_lifetime_retained = true
303
+ return
304
+ end
305
+
306
+ owner = @device || @encoder || @texture
307
+ attach_device_callback_lifetime(owner)
308
+ end
309
+
310
+ def release_device_callback_lifetime
311
+ return unless @device_callback_lifetime_retained
312
+
313
+ @device_callback_lifetime_retained = false
314
+ @device_callback_lifetime.release
315
+ end
316
+
317
+ def device_callback_lifetime_lease
318
+ lifetime = @device_callback_lifetime
319
+ return proc {} unless lifetime
320
+
321
+ lifetime.retain
322
+ released = false
323
+ mutex = Mutex.new
324
+ proc do
325
+ should_release = mutex.synchronize do
326
+ next false if released
327
+
328
+ released = true
329
+ true
330
+ end
331
+ lifetime.release if should_release
332
+ end
333
+ end
334
+
335
+ def ensure_not_released!
336
+ return unless released?
337
+
338
+ label_text = @label ? " (#{@label})" : ""
339
+ raise ResourceError, "#{self.class}#{label_text} has been released"
340
+ end
341
+ end
342
+ end
@@ -4,6 +4,11 @@ module WGPU
4
4
  class BindGroup
5
5
  attr_reader :handle
6
6
 
7
+ # Creates bindings between shader slots and GPU resources.
8
+ # @param device [Device] owning device
9
+ # @param layout [BindGroupLayout] layout the entries must match
10
+ # @param entries [Array<Hash>] resource binding descriptors
11
+ # @raise [PipelineError] if native validation or creation fails
7
12
  def initialize(device, label: nil, layout:, entries:)
8
13
  @device = device
9
14
 
@@ -38,6 +43,10 @@ module WGPU
38
43
  end
39
44
  end
40
45
 
46
+ # Releases the native bind group handle.
47
+ #
48
+ # Calling this method more than once has no effect.
49
+ # @return [void]
41
50
  def release
42
51
  return if @handle.null?
43
52
  Native.wgpuBindGroupRelease(@handle)
@@ -47,6 +56,18 @@ module WGPU
47
56
  private
48
57
 
49
58
  def create_entry(entry_hash)
59
+ DescriptorHelpers.validate_keys!(
60
+ entry_hash,
61
+ allowed: %i[binding buffer offset size sampler texture_view],
62
+ required: [:binding],
63
+ context: "bind group entry"
64
+ )
65
+ resources = %i[buffer sampler texture_view].select { |key| entry_hash[key] }
66
+ unless resources.one?
67
+ raise ArgumentError,
68
+ "bind group entry must define exactly one resource (:buffer, :sampler, or :texture_view)"
69
+ end
70
+
50
71
  entry = Native::BindGroupEntry.new
51
72
  entry[:next_in_chain] = nil
52
73
  entry[:binding] = entry_hash[:binding]
@@ -4,39 +4,30 @@ module WGPU
4
4
  class BindGroupLayout
5
5
  attr_reader :handle
6
6
 
7
- def self.from_handle(handle)
8
- layout = allocate
9
- layout.instance_variable_set(:@handle, handle)
10
- layout.instance_variable_set(:@device, nil)
7
+ # Wraps a native bind group layout handle owned by the caller.
8
+ #
9
+ # @param handle [FFI::Pointer] native bind group layout handle
10
+ # @param device [Device, nil] device whose callbacks the layout may use
11
+ # @return [BindGroupLayout] adopted wrapper
12
+ def self.from_handle(handle, device: nil)
13
+ layout = adopt_native_handle(handle)
14
+ layout.instance_variable_set(:@device, device)
15
+ layout.send(:attach_device_callback_lifetime, device)
11
16
  layout
12
17
  end
13
18
 
19
+ # Creates a layout describing shader-visible resource bindings.
20
+ # @param device [Device] owning device
21
+ # @param entries [Array<Hash>] binding layout descriptors
22
+ # @raise [PipelineError] if native validation or creation fails
14
23
  def initialize(device, label: nil, entries:)
15
24
  @device = device
16
-
17
- entries_array = entries.map { |e| create_entry(e) }
18
- entries_ptr = FFI::MemoryPointer.new(Native::BindGroupLayoutEntry, entries_array.size)
19
- entries_array.each_with_index do |entry, i|
20
- offset = i * Native::BindGroupLayoutEntry.size
21
- (entries_ptr + offset).put_bytes(0, entry.pointer.read_bytes(Native::BindGroupLayoutEntry.size))
22
- end
23
-
24
- desc = Native::BindGroupLayoutDescriptor.new
25
- desc[:next_in_chain] = nil
26
- if label
27
- label_ptr = FFI::MemoryPointer.from_string(label)
28
- desc[:label][:data] = label_ptr
29
- desc[:label][:length] = label.bytesize
30
- else
31
- desc[:label][:data] = nil
32
- desc[:label][:length] = 0
33
- end
34
- desc[:entry_count] = entries_array.size
35
- desc[:entries] = entries_ptr
25
+ desc, @descriptor_keepalive = build_descriptor(label:, entries:)
36
26
 
37
27
  device.push_error_scope(:validation)
38
28
  @handle = Native.wgpuDeviceCreateBindGroupLayout(device.handle, desc)
39
29
  error = device.pop_error_scope
30
+ @descriptor_keepalive = nil
40
31
 
41
32
  if @handle.null? || (error[:type] && error[:type] != :no_error)
42
33
  msg = error[:message] || "Failed to create bind group layout"
@@ -44,6 +35,10 @@ module WGPU
44
35
  end
45
36
  end
46
37
 
38
+ # Releases the native bind group layout handle.
39
+ #
40
+ # Calling this method more than once has no effect.
41
+ # @return [void]
47
42
  def release
48
43
  return if @handle.null?
49
44
  Native.wgpuBindGroupLayoutRelease(@handle)
@@ -52,7 +47,39 @@ module WGPU
52
47
 
53
48
  private
54
49
 
50
+ def build_descriptor(label:, entries:)
51
+ keepalive = []
52
+ entries_array = entries.map { |entry| create_entry(entry) }
53
+ entries_ptr = FFI::MemoryPointer.new(Native::BindGroupLayoutEntry, entries_array.size)
54
+ entries_array.each_with_index do |entry, index|
55
+ offset = index * Native::BindGroupLayoutEntry.size
56
+ (entries_ptr + offset).put_bytes(0, entry.pointer.read_bytes(Native::BindGroupLayoutEntry.size))
57
+ end
58
+ keepalive.concat(entries_array)
59
+ keepalive << entries_ptr
60
+
61
+ desc = Native::BindGroupLayoutDescriptor.new
62
+ desc[:next_in_chain] = nil
63
+ DescriptorHelpers.set_label(desc, label, keepalive:)
64
+ desc[:entry_count] = entries_array.size
65
+ desc[:entries] = entries_ptr
66
+ [desc, keepalive]
67
+ end
68
+
55
69
  def create_entry(entry_hash)
70
+ DescriptorHelpers.validate_keys!(
71
+ entry_hash,
72
+ allowed: %i[binding visibility buffer sampler texture storage_texture],
73
+ required: %i[binding visibility],
74
+ context: "bind group layout entry"
75
+ )
76
+ variants = %i[buffer sampler texture storage_texture].select { |key| entry_hash[key] }
77
+ unless variants.one?
78
+ raise ArgumentError,
79
+ "bind group layout entry must define exactly one resource variant " \
80
+ "(:buffer, :sampler, :texture, or :storage_texture)"
81
+ end
82
+
56
83
  entry = Native::BindGroupLayoutEntry.new
57
84
  entry[:next_in_chain] = nil
58
85
  entry[:binding] = entry_hash[:binding]
@@ -78,44 +105,84 @@ module WGPU
78
105
 
79
106
  if entry_hash[:buffer]
80
107
  buffer_info = entry_hash[:buffer]
81
- entry[:buffer][:type] = buffer_info[:type] || :storage
108
+ DescriptorHelpers.validate_keys!(
109
+ buffer_info,
110
+ allowed: %i[type has_dynamic_offset min_binding_size],
111
+ context: "buffer binding layout"
112
+ )
113
+ entry[:buffer][:type] = Native::EnumHelper.coerce(
114
+ Native::BufferBindingType,
115
+ buffer_info[:type] || :storage,
116
+ name: "buffer binding type"
117
+ )
82
118
  entry[:buffer][:has_dynamic_offset] = buffer_info[:has_dynamic_offset] ? 1 : 0
83
119
  entry[:buffer][:min_binding_size] = buffer_info[:min_binding_size] || 0
84
120
  end
85
121
 
86
122
  if entry_hash[:sampler]
87
123
  sampler_info = entry_hash[:sampler]
88
- entry[:sampler][:type] = sampler_info[:type] || :filtering
124
+ DescriptorHelpers.validate_keys!(
125
+ sampler_info,
126
+ allowed: [:type],
127
+ context: "sampler binding layout"
128
+ )
129
+ entry[:sampler][:type] = Native::EnumHelper.coerce(
130
+ Native::SamplerBindingType,
131
+ sampler_info[:type] || :filtering,
132
+ name: "sampler binding type"
133
+ )
89
134
  end
90
135
 
91
136
  if entry_hash[:texture]
92
137
  texture_info = entry_hash[:texture]
93
- entry[:texture][:sample_type] = texture_info[:sample_type] || :float
94
- entry[:texture][:view_dimension] = texture_info[:view_dimension] || :d2
138
+ DescriptorHelpers.validate_keys!(
139
+ texture_info,
140
+ allowed: %i[sample_type view_dimension multisampled],
141
+ context: "texture binding layout"
142
+ )
143
+ entry[:texture][:sample_type] = Native::EnumHelper.coerce(
144
+ Native::TextureSampleType,
145
+ texture_info[:sample_type] || :float,
146
+ name: "texture sample type"
147
+ )
148
+ entry[:texture][:view_dimension] = Native::EnumHelper.coerce(
149
+ Native::TextureViewDimension,
150
+ texture_info[:view_dimension] || :d2,
151
+ name: "texture view dimension"
152
+ )
95
153
  entry[:texture][:multisampled] = texture_info[:multisampled] ? 1 : 0
96
154
  end
97
155
 
98
156
  if entry_hash[:storage_texture]
99
157
  st_info = entry_hash[:storage_texture]
100
- entry[:storage_texture][:access] = st_info[:access] || :write_only
101
- entry[:storage_texture][:format] = st_info[:format]
102
- entry[:storage_texture][:view_dimension] = st_info[:view_dimension] || :d2
158
+ DescriptorHelpers.validate_keys!(
159
+ st_info,
160
+ allowed: %i[access format view_dimension],
161
+ required: [:format],
162
+ context: "storage texture binding layout"
163
+ )
164
+ entry[:storage_texture][:access] = Native::EnumHelper.coerce(
165
+ Native::StorageTextureAccess,
166
+ st_info[:access] || :write_only,
167
+ name: "storage texture access"
168
+ )
169
+ entry[:storage_texture][:format] = Native::EnumHelper.coerce(
170
+ Native::TextureFormat,
171
+ st_info.fetch(:format),
172
+ name: "storage texture format"
173
+ )
174
+ entry[:storage_texture][:view_dimension] = Native::EnumHelper.coerce(
175
+ Native::TextureViewDimension,
176
+ st_info[:view_dimension] || :d2,
177
+ name: "texture view dimension"
178
+ )
103
179
  end
104
180
 
105
181
  entry
106
182
  end
107
183
 
108
184
  def normalize_visibility(visibility)
109
- case visibility
110
- when Integer
111
- visibility
112
- when Symbol
113
- Native::ShaderStage[visibility]
114
- when Array
115
- visibility.reduce(0) { |acc, v| acc | Native::ShaderStage[v] }
116
- else
117
- raise ArgumentError, "Invalid visibility: #{visibility}"
118
- end
185
+ Native::EnumHelper.coerce_flags(Native::ShaderStage, visibility, name: "shader visibility")
119
186
  end
120
187
  end
121
188
  end
@@ -4,34 +4,14 @@ module WGPU
4
4
  class ComputePipeline
5
5
  attr_reader :handle
6
6
 
7
+ # Creates a compute pipeline.
8
+ # @param device [Device] owning device
9
+ # @param layout [PipelineLayout, :auto, nil] pipeline layout
10
+ # @param compute [Hash] programmable compute stage descriptor
11
+ # @raise [PipelineError] if native validation or creation fails
7
12
  def initialize(device, label: nil, layout:, compute:)
8
13
  @device = device
9
- @pointers = []
10
-
11
- entry_point = compute[:entry_point] || "main"
12
- entry_point_ptr = FFI::MemoryPointer.from_string(entry_point)
13
- @pointers << entry_point_ptr
14
-
15
- desc = Native::ComputePipelineDescriptor.new
16
- desc[:next_in_chain] = nil
17
- if label
18
- label_ptr = FFI::MemoryPointer.from_string(label)
19
- @pointers << label_ptr
20
- desc[:label][:data] = label_ptr
21
- desc[:label][:length] = label.bytesize
22
- else
23
- desc[:label][:data] = nil
24
- desc[:label][:length] = 0
25
- end
26
- desc[:layout] = normalize_layout(layout)
27
-
28
- desc[:compute][:next_in_chain] = nil
29
- desc[:compute][:module] = compute.fetch(:module).handle
30
- desc[:compute][:entry_point][:data] = entry_point_ptr
31
- desc[:compute][:entry_point][:length] = entry_point.bytesize
32
- desc[:compute][:constant_count] = 0
33
- desc[:compute][:constants] = nil
34
- setup_constants(desc[:compute], compute[:constants])
14
+ desc, @pointers = build_descriptor(label:, layout:, compute:)
35
15
 
36
16
  device.push_error_scope(:validation)
37
17
  @handle = Native.wgpuDeviceCreateComputePipeline(device.handle, desc)
@@ -43,12 +23,20 @@ module WGPU
43
23
  end
44
24
  end
45
25
 
26
+ # Returns the bind group layout inferred or assigned at an index.
27
+ # @param index [Integer] bind group index
28
+ # @return [BindGroupLayout]
29
+ # @raise [PipelineError] if no native layout is returned
46
30
  def get_bind_group_layout(index)
47
31
  handle = Native.wgpuComputePipelineGetBindGroupLayout(@handle, index)
48
32
  raise PipelineError, "Failed to get bind group layout at index #{index}" if handle.null?
49
- BindGroupLayout.from_handle(handle)
33
+ BindGroupLayout.from_handle(handle, device: @device)
50
34
  end
51
35
 
36
+ # Releases the native compute pipeline handle.
37
+ #
38
+ # Calling this method more than once has no effect.
39
+ # @return [void]
52
40
  def release
53
41
  return if @handle.null?
54
42
  Native.wgpuComputePipelineRelease(@handle)
@@ -57,32 +45,34 @@ module WGPU
57
45
 
58
46
  private
59
47
 
48
+ def build_descriptor(label:, layout:, compute:)
49
+ DescriptorHelpers.validate_keys!(
50
+ compute,
51
+ allowed: %i[module entry_point constants],
52
+ required: [:module],
53
+ context: "compute pipeline stage"
54
+ )
55
+ @pointers = []
56
+
57
+ desc = Native::ComputePipelineDescriptor.new
58
+ desc[:next_in_chain] = nil
59
+ DescriptorHelpers.set_label(desc, label, keepalive: @pointers)
60
+ desc[:layout] = normalize_layout(layout)
61
+ desc[:compute][:next_in_chain] = nil
62
+ desc[:compute][:module] = compute.fetch(:module).handle
63
+ DescriptorHelpers.set_nullable_string_view(
64
+ desc[:compute][:entry_point],
65
+ compute[:entry_point],
66
+ keepalive: @pointers
67
+ )
68
+ DescriptorHelpers.set_constants(desc[:compute], compute[:constants], keepalive: @pointers)
69
+
70
+ [desc, @pointers]
71
+ end
72
+
60
73
  def normalize_layout(layout)
61
74
  return nil if layout.nil? || layout == :auto || layout == "auto"
62
75
  layout.handle
63
76
  end
64
-
65
- def setup_constants(stage_desc, constants)
66
- return if constants.nil? || constants.empty?
67
-
68
- constants_ptr = FFI::MemoryPointer.new(Native::ConstantEntry, constants.size)
69
- @pointers << constants_ptr
70
-
71
- constants.each_with_index do |(key, value), i|
72
- entry_ptr = constants_ptr + (i * Native::ConstantEntry.size)
73
- entry = Native::ConstantEntry.new(entry_ptr)
74
- entry[:next_in_chain] = nil
75
-
76
- key_str = key.to_s
77
- key_ptr = FFI::MemoryPointer.from_string(key_str)
78
- @pointers << key_ptr
79
- entry[:key][:data] = key_ptr
80
- entry[:key][:length] = key_str.bytesize
81
- entry[:value] = value.to_f
82
- end
83
-
84
- stage_desc[:constant_count] = constants.size
85
- stage_desc[:constants] = constants_ptr
86
- end
87
77
  end
88
78
  end