wgpu 1.2.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/README.md +18 -2
  4. data/docs/README.md +2 -2
  5. data/docs/api_coverage.md +23 -15
  6. data/docs/async.md +10 -0
  7. data/docs/command_encoding.md +25 -0
  8. data/docs/getting_started_compute.md +1 -0
  9. data/docs/pipeline_descriptors.md +7 -2
  10. data/docs/releasing.md +11 -1
  11. data/docs/resource_lifetime.md +16 -2
  12. data/docs/texture_readback.md +27 -0
  13. data/docs/troubleshooting.md +5 -0
  14. data/docs/upgrading_wgpu_native.md +16 -5
  15. data/lib/wgpu/async_task.rb +19 -0
  16. data/lib/wgpu/commands/command_buffer.rb +14 -1
  17. data/lib/wgpu/commands/command_encoder.rb +58 -1
  18. data/lib/wgpu/commands/compute_pass.rb +43 -0
  19. data/lib/wgpu/commands/render_bundle.rb +9 -1
  20. data/lib/wgpu/commands/render_bundle_encoder.rb +55 -1
  21. data/lib/wgpu/commands/render_pass.rb +102 -0
  22. data/lib/wgpu/core/adapter.rb +91 -12
  23. data/lib/wgpu/core/async_waiter.rb +15 -0
  24. data/lib/wgpu/core/canvas_context.rb +32 -0
  25. data/lib/wgpu/core/device.rb +296 -46
  26. data/lib/wgpu/core/instance.rb +20 -0
  27. data/lib/wgpu/core/queue.rb +139 -24
  28. data/lib/wgpu/core/surface.rb +49 -1
  29. data/lib/wgpu/data_types.rb +16 -0
  30. data/lib/wgpu/descriptor_helpers.rb +65 -0
  31. data/lib/wgpu/error.rb +15 -0
  32. data/lib/wgpu/native/abi_verifier.rb +37 -3
  33. data/lib/wgpu/native/callbacks.rb +6 -0
  34. data/lib/wgpu/native/capabilities.rb +16 -2
  35. data/lib/wgpu/native/distribution.rb +63 -0
  36. data/lib/wgpu/native/enum_helper.rb +17 -0
  37. data/lib/wgpu/native/enums.rb +8 -0
  38. data/lib/wgpu/native/fixtures/webgpu-v27.0.4.0-enums.h +848 -0
  39. data/lib/wgpu/native/functions.rb +11 -0
  40. data/lib/wgpu/native/installer.rb +48 -17
  41. data/lib/wgpu/native/loader.rb +22 -0
  42. data/lib/wgpu/native/structs.rb +18 -1
  43. data/lib/wgpu/native_resource.rb +174 -3
  44. data/lib/wgpu/pipeline/bind_group.rb +9 -0
  45. data/lib/wgpu/pipeline/bind_group_layout.rb +17 -4
  46. data/lib/wgpu/pipeline/compute_pipeline.rb +20 -32
  47. data/lib/wgpu/pipeline/pipeline_layout.rb +8 -0
  48. data/lib/wgpu/pipeline/render_pipeline.rb +27 -42
  49. data/lib/wgpu/pipeline/shader_module.rb +54 -29
  50. data/lib/wgpu/resources/buffer.rb +258 -31
  51. data/lib/wgpu/resources/query_set.rb +12 -0
  52. data/lib/wgpu/resources/sampler.rb +7 -0
  53. data/lib/wgpu/resources/texture.rb +45 -6
  54. data/lib/wgpu/resources/texture_view.rb +24 -4
  55. data/lib/wgpu/texture_format.rb +14 -0
  56. data/lib/wgpu/version.rb +1 -1
  57. data/lib/wgpu/window.rb +26 -0
  58. data/sig/wgpu.rbs +84 -5
  59. metadata +3 -1
@@ -4,11 +4,18 @@ module WGPU
4
4
  class Queue
5
5
  attr_reader :handle
6
6
 
7
+ # Wraps a device's native submission queue.
8
+ # @param handle [FFI::Pointer] native queue handle
9
+ # @param device [Device, nil] owning device
7
10
  def initialize(handle, device: nil)
8
11
  @handle = handle
9
12
  @device = device
10
13
  end
11
14
 
15
+ # Submits command buffers exactly once in the supplied order.
16
+ # @param command_buffers [CommandBuffer, Array<CommandBuffer>] buffers to submit
17
+ # @return [void]
18
+ # @raise [CommandError] for duplicate or previously submitted buffers
12
19
  def submit(command_buffers)
13
20
  buffers = Array(command_buffers)
14
21
  return if buffers.empty?
@@ -28,6 +35,11 @@ module WGPU
28
35
  buffers.each(&:mark_submitted!)
29
36
  end
30
37
 
38
+ # Writes typed data into a GPU buffer.
39
+ # @param buffer [Buffer] destination buffer
40
+ # @param buffer_offset [Integer] destination byte offset
41
+ # @param data [Array, String, FFI::Pointer] source data
42
+ # @return [void]
31
43
  def write_buffer(buffer, buffer_offset, data, data_offset: 0, size: nil, type: :f32)
32
44
  data_ptr, byte_size = DataTypes.to_pointer(data, type:)
33
45
  DataTypes.validate_alignment!(buffer_offset, 4, name: "buffer_offset")
@@ -49,6 +61,12 @@ module WGPU
49
61
  )
50
62
  end
51
63
 
64
+ # Writes typed data into a texture region.
65
+ # @param destination [Hash] destination texture and origin
66
+ # @param data [Array, String, FFI::Pointer] source data
67
+ # @param data_layout [Hash] source byte layout
68
+ # @param size [Hash, Array] extent to write
69
+ # @return [void]
52
70
  def write_texture(destination:, data:, data_layout:, size:, type: :f32)
53
71
  data_ptr, byte_size = DataTypes.to_pointer(data, type:)
54
72
 
@@ -83,6 +101,10 @@ module WGPU
83
101
  Native.wgpuQueueWriteTexture(@handle, dst, data_ptr, byte_size, layout, extent)
84
102
  end
85
103
 
104
+ # Copies a GPU buffer to mapped staging memory and returns its bytes.
105
+ # @param buffer [Buffer] source buffer
106
+ # @param staging [Buffer, nil] reusable map-read staging buffer
107
+ # @return [String] copied bytes
86
108
  def read_buffer(buffer, offset: 0, size: nil, device: nil, staging: nil)
87
109
  device ||= @device
88
110
  raise ArgumentError, "device is required when the queue has no owning device" unless device
@@ -90,6 +112,7 @@ module WGPU
90
112
  size ||= buffer.size - offset
91
113
  owns_staging = staging.nil?
92
114
  staging ||= Buffer.new(device, size: size, usage: [:map_read, :copy_dst])
115
+ validate_readback_staging!(staging, size)
93
116
  encoder = CommandEncoder.new(device)
94
117
  encoder.copy_buffer_to_buffer(
95
118
  source: buffer,
@@ -101,15 +124,26 @@ module WGPU
101
124
  command_buffer = encoder.finish
102
125
  submit([command_buffer])
103
126
 
127
+ map_requested = true
104
128
  staging.map_sync(:read)
105
129
  staging.read_mapped_data(size:)
106
130
  ensure
107
- staging&.unmap if staging && staging.map_state == :mapped
108
- command_buffer&.release
109
- encoder&.release
110
- staging&.release if owns_staging
131
+ cleanup_readback_resources(
132
+ staging:,
133
+ unmap_staging: map_requested,
134
+ owns_staging:,
135
+ command_buffer:,
136
+ encoder:,
137
+ active_error: $!
138
+ )
111
139
  end
112
140
 
141
+ # Copies a texture region to mapped staging memory and returns its padded bytes.
142
+ # @param source [Hash] source texture, origin, aspect, and optional format
143
+ # @param data_layout [Hash] destination byte layout
144
+ # @param size [Hash, Array] extent to read
145
+ # @param staging [Buffer, nil] reusable map-read staging buffer
146
+ # @return [String] copied bytes
113
147
  def read_texture(source:, data_layout:, size:, device: nil, staging: nil)
114
148
  device ||= @device
115
149
  raise ArgumentError, "device is required when the queue has no owning device" unless device
@@ -125,10 +159,12 @@ module WGPU
125
159
  )
126
160
  format = source[:format] || source[:texture].format
127
161
  aspect = source[:aspect] || :all
128
- minimum_bytes_per_row = TextureFormat.bytes_per_row(width, format, aspect:)
162
+ tight_bytes_per_row = TextureFormat.bytes_per_row(width, format, aspect:)
163
+ minimum_bytes_per_row = TextureFormat.aligned_bytes_per_row(width, format, aspect:)
129
164
  if bytes_per_row < minimum_bytes_per_row
130
165
  raise ArgumentError,
131
- "bytes_per_row must be at least #{minimum_bytes_per_row} for width #{width} and #{format.inspect}"
166
+ "bytes_per_row must be at least #{minimum_bytes_per_row} for width #{width} and #{format.inspect} " \
167
+ "(tight row is #{tight_bytes_per_row} bytes)"
132
168
  end
133
169
 
134
170
  rows_per_image = data_layout[:rows_per_image] || height
@@ -136,6 +172,7 @@ module WGPU
136
172
 
137
173
  owns_staging = staging.nil?
138
174
  staging ||= Buffer.new(device, size: buffer_size, usage: [:map_read, :copy_dst])
175
+ validate_readback_staging!(staging, buffer_size)
139
176
  encoder = CommandEncoder.new(device)
140
177
  encoder.copy_texture_to_buffer(
141
178
  source: source,
@@ -150,25 +187,41 @@ module WGPU
150
187
  command_buffer = encoder.finish
151
188
  submit([command_buffer])
152
189
 
190
+ map_requested = true
153
191
  staging.map_sync(:read)
154
192
  staging.read_mapped_data(size: buffer_size)
155
193
  ensure
156
- staging&.unmap if staging && staging.map_state == :mapped
157
- command_buffer&.release
158
- encoder&.release
159
- staging&.release if owns_staging
194
+ cleanup_readback_resources(
195
+ staging:,
196
+ unmap_staging: map_requested,
197
+ owns_staging:,
198
+ command_buffer:,
199
+ encoder:,
200
+ active_error: $!
201
+ )
160
202
  end
161
203
 
204
+ # Waits until all previously submitted queue work completes.
205
+ # @param device [Device, nil] device used to drive callback progress
206
+ # @param timeout [Numeric, nil] maximum wait time in seconds
207
+ # @return [Symbol] native completion status
162
208
  def on_submitted_work_done(device: nil, timeout: nil)
163
209
  device ||= @device
164
210
  instance = device&.adapter&.instance
165
211
  status_holder = { done: false, status: nil }
166
212
 
213
+ callback_lifetime_release = device_callback_lifetime_lease
214
+ callback_token = nil
167
215
  callback = FFI::Function.new(
168
216
  :void, [:uint32, :pointer, :pointer]
169
217
  ) do |status, _userdata1, _userdata2|
170
- status_holder[:done] = true
171
- status_holder[:status] = Native::QueueWorkDoneStatus[status]
218
+ begin
219
+ status_holder[:status] = Native::QueueWorkDoneStatus[status]
220
+ status_holder[:done] = true
221
+ ensure
222
+ CallbackKeepalive.release(self, callback_token)
223
+ callback_lifetime_release.call
224
+ end
172
225
  end
173
226
 
174
227
  callback_info = Native::QueueWorkDoneCallbackInfo.new
@@ -179,28 +232,37 @@ module WGPU
179
232
  callback_info[:userdata2] = nil
180
233
 
181
234
  callback_token = CallbackKeepalive.retain(self, callback)
182
- begin
183
- future = Native.wgpuQueueOnSubmittedWorkDone(@handle, callback_info)
184
- AsyncWaiter.wait(
185
- status_holder: status_holder,
186
- instance: instance,
187
- device: device,
188
- future: future,
189
- timeout: timeout
190
- )
191
- ensure
192
- CallbackKeepalive.release(self, callback_token)
193
- end
235
+ future =
236
+ begin
237
+ Native.wgpuQueueOnSubmittedWorkDone(@handle, callback_info)
238
+ rescue StandardError
239
+ CallbackKeepalive.release(self, callback_token)
240
+ callback_lifetime_release.call
241
+ raise
242
+ end
243
+ AsyncWaiter.wait(
244
+ status_holder: status_holder,
245
+ instance: instance,
246
+ device: device,
247
+ future: future,
248
+ timeout: timeout
249
+ )
194
250
 
195
251
  status_holder[:status]
196
252
  end
197
253
 
254
+ # Waits for prior queue work on a background task.
255
+ # @return [AsyncTask] task yielding the native completion status
198
256
  def on_submitted_work_done_async(device: nil, timeout: nil)
199
257
  AsyncTask.new do
200
258
  on_submitted_work_done(device: device, timeout: timeout)
201
259
  end
202
260
  end
203
261
 
262
+ # Releases the native queue handle.
263
+ #
264
+ # Calling this method more than once has no effect.
265
+ # @return [void]
204
266
  def release
205
267
  return if @handle.null?
206
268
  Native.wgpuQueueRelease(@handle)
@@ -217,5 +279,58 @@ module WGPU
217
279
  end
218
280
  end
219
281
 
282
+ def validate_readback_staging!(staging, required_size)
283
+ if staging.size < required_size
284
+ raise ArgumentError,
285
+ "staging buffer size must be at least #{required_size} bytes (got #{staging.size})"
286
+ end
287
+
288
+ required_usage = Native::BufferUsage.fetch(:map_read) | Native::BufferUsage.fetch(:copy_dst)
289
+ unless (staging.usage & required_usage) == required_usage
290
+ raise ArgumentError, "staging buffer usage must include :map_read and :copy_dst"
291
+ end
292
+
293
+ return if staging.map_state == :unmapped
294
+
295
+ raise ArgumentError, "staging buffer must be unmapped before readback"
296
+ end
297
+
298
+ def cleanup_readback_resources(
299
+ staging:,
300
+ unmap_staging:,
301
+ owns_staging:,
302
+ command_buffer:,
303
+ encoder:,
304
+ active_error:
305
+ )
306
+ cleanup_error = nil
307
+
308
+ begin
309
+ staging&.unmap if unmap_staging && staging && staging.map_state == :mapped
310
+ rescue StandardError => e
311
+ cleanup_error ||= e
312
+ ensure
313
+ begin
314
+ command_buffer&.release
315
+ rescue StandardError => e
316
+ cleanup_error ||= e
317
+ ensure
318
+ begin
319
+ encoder&.release
320
+ rescue StandardError => e
321
+ cleanup_error ||= e
322
+ ensure
323
+ begin
324
+ staging&.release if owns_staging
325
+ rescue StandardError => e
326
+ cleanup_error ||= e
327
+ end
328
+ end
329
+ end
330
+ end
331
+
332
+ raise cleanup_error if cleanup_error && active_error.nil?
333
+ end
334
+
220
335
  end
221
336
  end
@@ -4,6 +4,9 @@ module WGPU
4
4
  class Surface
5
5
  attr_reader :handle
6
6
 
7
+ # Creates a surface backed by a Core Animation Metal layer.
8
+ # @return [Surface]
9
+ # @raise [SurfaceError] if native surface creation fails
7
10
  def self.from_metal_layer(instance, layer)
8
11
  source = Native::SurfaceSourceMetalLayer.new
9
12
  source[:chain][:next] = nil
@@ -21,6 +24,9 @@ module WGPU
21
24
  new(handle, instance)
22
25
  end
23
26
 
27
+ # Creates a surface backed by a Windows window handle.
28
+ # @return [Surface]
29
+ # @raise [SurfaceError] if native surface creation fails
24
30
  def self.from_windows_hwnd(instance, hinstance, hwnd)
25
31
  source = Native::SurfaceSourceWindowsHWND.new
26
32
  source[:chain][:next] = nil
@@ -39,6 +45,9 @@ module WGPU
39
45
  new(handle, instance)
40
46
  end
41
47
 
48
+ # Creates a surface backed by an Xlib window.
49
+ # @return [Surface]
50
+ # @raise [SurfaceError] if native surface creation fails
42
51
  def self.from_xlib_window(instance, display, window)
43
52
  source = Native::SurfaceSourceXlibWindow.new
44
53
  source[:chain][:next] = nil
@@ -57,6 +66,9 @@ module WGPU
57
66
  new(handle, instance)
58
67
  end
59
68
 
69
+ # Creates a surface backed by a Wayland surface.
70
+ # @return [Surface]
71
+ # @raise [SurfaceError] if native surface creation fails
60
72
  def self.from_wayland_surface(instance, display, surface)
61
73
  source = Native::SurfaceSourceWaylandSurface.new
62
74
  source[:chain][:next] = nil
@@ -75,6 +87,9 @@ module WGPU
75
87
  new(handle, instance)
76
88
  end
77
89
 
90
+ # Wraps a native presentation surface.
91
+ # @param handle [FFI::Pointer] native surface handle
92
+ # @param instance [Instance] owning instance
78
93
  def initialize(handle, instance)
79
94
  @handle = handle
80
95
  @instance = instance
@@ -82,6 +97,8 @@ module WGPU
82
97
  @config = nil
83
98
  end
84
99
 
100
+ # Configures this surface for presentation by a device.
101
+ # @return [Hash] stored configuration
85
102
  def configure(device:, format:, usage: :render_attachment, width:, height:, present_mode: :fifo, alpha_mode: :auto, view_formats: [])
86
103
  config = Native::SurfaceConfiguration.new
87
104
  config[:next_in_chain] = nil
@@ -114,8 +131,10 @@ module WGPU
114
131
  )
115
132
 
116
133
  Native.wgpuSurfaceConfigure(@handle, config)
134
+ release_device_callback_lifetime
117
135
  @configured = true
118
136
  @device = device
137
+ attach_device_callback_lifetime(device)
119
138
  @config = {
120
139
  device: device,
121
140
  format: format,
@@ -128,12 +147,20 @@ module WGPU
128
147
  }
129
148
  end
130
149
 
150
+ # Removes the current surface configuration.
151
+ # @return [void]
131
152
  def unconfigure
132
153
  Native.wgpuSurfaceUnconfigure(@handle)
154
+ release_device_callback_lifetime
133
155
  @configured = false
156
+ @device = nil
134
157
  @config = nil
135
158
  end
136
159
 
160
+ # Acquires the current surface texture.
161
+ # @return [Texture]
162
+ # @raise [SurfaceError] if unconfigured or no texture is returned
163
+ # @raise [SurfaceAcquisitionError] if the surface status is unsuccessful
137
164
  def current_texture
138
165
  raise SurfaceError, "Surface is not configured" unless @configured
139
166
 
@@ -150,26 +177,43 @@ module WGPU
150
177
  raise SurfaceError, "Surface returned null texture"
151
178
  end
152
179
 
153
- Texture.from_handle(texture_ptr, surface_status: status)
180
+ Texture.from_handle(texture_ptr, surface_status: status, device: @device)
154
181
  end
155
182
 
183
+ # Acquires the texture for the current presentation frame.
184
+ #
185
+ # @return [Texture] acquired surface texture
186
+ # @raise [SurfaceError] if the surface is not configured
187
+ # @raise [SurfaceAcquisitionError] if acquisition fails
156
188
  def get_current_texture
157
189
  current_texture
158
190
  end
159
191
 
192
+ # Presents the current surface texture.
193
+ # @return [void]
160
194
  def present
161
195
  Native.wgpuSurfacePresent(@handle)
162
196
  end
163
197
 
198
+ # Returns the most recent surface configuration.
199
+ #
200
+ # @return [Hash, nil] configuration options, or +nil+ when unconfigured
164
201
  def get_configuration
165
202
  @config
166
203
  end
167
204
 
205
+ # Returns the first surface format supported by an adapter.
206
+ #
207
+ # @param adapter [Adapter] adapter used to query surface capabilities
208
+ # @return [Symbol] preferred texture format
168
209
  def get_preferred_format(adapter)
169
210
  caps = capabilities(adapter)
170
211
  caps[:formats].first || :bgra8_unorm
171
212
  end
172
213
 
214
+ # Returns the formats, presentation modes, alpha modes, and usages supported by an adapter.
215
+ # @param adapter [Adapter] adapter to query
216
+ # @return [Hash]
173
217
  def capabilities(adapter)
174
218
  caps = Native::SurfaceCapabilities.new
175
219
  Native.wgpuSurfaceGetCapabilities(@handle, adapter.handle, caps)
@@ -205,6 +249,10 @@ module WGPU
205
249
  Native.wgpuSurfaceCapabilitiesFreeMembers(caps) if caps
206
250
  end
207
251
 
252
+ # Releases the native surface handle.
253
+ #
254
+ # Calling this method more than once has no effect.
255
+ # @return [void]
208
256
  def release
209
257
  return if @handle.null?
210
258
  Native.wgpuSurfaceRelease(@handle)
@@ -13,6 +13,10 @@ module WGPU
13
13
 
14
14
  module_function
15
15
 
16
+ # Encodes Ruby values in little-endian native transfer format.
17
+ # @param values [Array] values to encode
18
+ # @param type [Symbol] element type
19
+ # @return [String] encoded bytes
16
20
  def pack(values, type: :f32)
17
21
  format, = format_for(type)
18
22
  Array(values).pack(format)
@@ -20,6 +24,10 @@ module WGPU
20
24
  raise ArgumentError, "value is out of range for #{type}: #{e.message}"
21
25
  end
22
26
 
27
+ # Decodes little-endian transfer bytes into Ruby values.
28
+ # @param bytes [String] encoded bytes
29
+ # @param type [Symbol] element type
30
+ # @return [Array]
23
31
  def unpack(bytes, type: :f32)
24
32
  format, byte_size = format_for(type)
25
33
  unless (bytes.bytesize % byte_size).zero?
@@ -29,10 +37,15 @@ module WGPU
29
37
  bytes.unpack(format)
30
38
  end
31
39
 
40
+ # Returns the byte width of one typed value.
41
+ # @param type [Symbol] element type
42
+ # @return [Integer]
32
43
  def byte_size(type)
33
44
  format_for(type).last
34
45
  end
35
46
 
47
+ # Converts data to an FFI pointer and reports its byte length.
48
+ # @return [Array(FFI::Pointer, Integer)] pointer and byte length
36
49
  def to_pointer(data, type: :f32)
37
50
  return [data, pointer_size(data)] if data.is_a?(FFI::Pointer)
38
51
 
@@ -42,6 +55,9 @@ module WGPU
42
55
  [pointer, bytes.bytesize]
43
56
  end
44
57
 
58
+ # Validates a non-negative aligned byte value.
59
+ # @return [Integer] normalized value
60
+ # @raise [ArgumentError] if negative or misaligned
45
61
  def validate_alignment!(value, alignment, name:)
46
62
  integer = Integer(value)
47
63
  raise ArgumentError, "#{name} must be non-negative" if integer.negative?
@@ -2,8 +2,16 @@
2
2
 
3
3
  module WGPU
4
4
  module DescriptorHelpers
5
+ SIZE_MAX = (1 << (FFI.type_size(:size_t) * 8)) - 1
6
+
5
7
  module_function
6
8
 
9
+ # Writes an optional Ruby label into a native descriptor.
10
+ #
11
+ # @param descriptor [FFI::Struct] descriptor with a +label+ member
12
+ # @param label [String, nil] label text
13
+ # @param keepalive [Array] receives allocated pointers that must remain alive
14
+ # @return [void]
7
15
  def set_label(descriptor, label, keepalive:)
8
16
  if label
9
17
  pointer = FFI::MemoryPointer.from_string(label)
@@ -16,6 +24,11 @@ module WGPU
16
24
  end
17
25
  end
18
26
 
27
+ # Allocates and fills a native uint32 array.
28
+ #
29
+ # @param values [Array<Integer>] values to copy
30
+ # @param keepalive [Array] receives the allocated pointer
31
+ # @return [FFI::MemoryPointer, nil] pointer, or +nil+ for an empty array
19
32
  def uint32_array(values, keepalive:)
20
33
  return nil if values.empty?
21
34
 
@@ -25,6 +38,58 @@ module WGPU
25
38
  pointer
26
39
  end
27
40
 
41
+ # Writes a nullable string into a native string view.
42
+ #
43
+ # @param string_view [Native::StringView] destination view
44
+ # @param value [String, nil] string value; +nil+ uses the native null sentinel
45
+ # @param keepalive [Array] receives allocated pointers that must remain alive
46
+ # @return [void]
47
+ def set_nullable_string_view(string_view, value, keepalive:)
48
+ if value.nil?
49
+ string_view[:data] = nil
50
+ string_view[:length] = SIZE_MAX
51
+ return
52
+ end
53
+
54
+ string = value
55
+ pointer = FFI::MemoryPointer.from_string(string)
56
+ keepalive << pointer
57
+ string_view[:data] = pointer
58
+ string_view[:length] = string.bytesize
59
+ end
60
+
61
+ # Writes pipeline-overridable constants into a stage descriptor.
62
+ #
63
+ # @param stage_descriptor [FFI::Struct] programmable stage descriptor
64
+ # @param constants [Hash{#to_s => Numeric}, nil] constant names and values
65
+ # @param keepalive [Array] receives allocated pointers that must remain alive
66
+ # @return [void]
67
+ def set_constants(stage_descriptor, constants, keepalive:)
68
+ stage_descriptor[:constant_count] = 0
69
+ stage_descriptor[:constants] = nil
70
+ return if constants.nil? || constants.empty?
71
+
72
+ constants_pointer = FFI::MemoryPointer.new(Native::ConstantEntry, constants.size)
73
+ keepalive << constants_pointer
74
+
75
+ constants.each_with_index do |(key, value), index|
76
+ entry_pointer = constants_pointer + (index * Native::ConstantEntry.size)
77
+ entry = Native::ConstantEntry.new(entry_pointer)
78
+ entry[:next_in_chain] = nil
79
+ set_nullable_string_view(entry[:key], key.to_s, keepalive:)
80
+ entry[:value] = value.to_f
81
+ end
82
+
83
+ stage_descriptor[:constant_count] = constants.size
84
+ stage_descriptor[:constants] = constants_pointer
85
+ end
86
+
87
+ # Checks required keys and warns about unsupported descriptor keys.
88
+ # @param options [Hash] descriptor options
89
+ # @param allowed [Array<Symbol>] supported keys
90
+ # @param required [Array<Symbol>] mandatory keys
91
+ # @return [Hash] original options
92
+ # @raise [ArgumentError] when required keys are missing
28
93
  def validate_keys!(options, allowed:, required: [], context: "descriptor")
29
94
  return options unless options.is_a?(Hash)
30
95
 
data/lib/wgpu/error.rb CHANGED
@@ -20,6 +20,9 @@ module WGPU
20
20
  class SurfaceAcquisitionError < SurfaceError
21
21
  attr_reader :status
22
22
 
23
+ # Creates an acquisition error for a native surface status.
24
+ # @param status [Symbol] native acquisition status
25
+ # @param message [String, nil] optional override message
23
26
  def initialize(status, message = nil)
24
27
  @status = status
25
28
  super(message || "Failed to get current surface texture: #{status}")
@@ -28,12 +31,17 @@ module WGPU
28
31
  class RenderBundleError < Error; end
29
32
 
30
33
  GPUError = Data.define(:type, :message) do
34
+ # Converts a native error-scope result into a typed error.
35
+ # @param error [Hash, nil] native error result
36
+ # @return [GPUError, nil]
31
37
  def self.from_hash(error)
32
38
  return if error.nil? || error[:type].nil? || error[:type] == :no_error
33
39
 
34
40
  new(type: error[:type], message: error[:message].to_s)
35
41
  end
36
42
 
43
+ # Returns the Ruby exception class matching this GPU error type.
44
+ # @return [Class<Error>]
37
45
  def exception_class
38
46
  {
39
47
  validation: ValidationError,
@@ -43,10 +51,15 @@ module WGPU
43
51
  }.fetch(type, Error)
44
52
  end
45
53
 
54
+ # Raises this error as its matching Ruby exception.
55
+ # @return [void]
56
+ # @raise [Error]
46
57
  def raise!
47
58
  raise exception_class, "GPU error (#{type}): #{message}"
48
59
  end
49
60
 
61
+ # Returns a serializable representation of the error.
62
+ # @return [Hash]
50
63
  def to_h
51
64
  { type:, message: }
52
65
  end
@@ -63,6 +76,8 @@ module WGPU
63
76
  alias line line_num
64
77
  alias column line_pos
65
78
 
79
+ # Formats the diagnostic with source location, severity, and message.
80
+ # @return [String]
66
81
  def to_s
67
82
  location = line_num&.positive? ? "#{line_num}:#{line_pos}" : "unknown location"
68
83
  "#{location}: #{type}: #{message}"
@@ -22,18 +22,29 @@ module WGPU
22
22
  ]
23
23
  }.freeze
24
24
 
25
+ # Creates a verifier for a header and native binding.
26
+ # @param header_path [String, nil] header fixture to compare
27
+ # @param native [Module] native binding module
25
28
  def initialize(header_path: nil, native: Native)
26
29
  @header_path = header_path || self.class.default_header_path
27
30
  @native = native
28
31
  end
29
32
 
33
+ # Verifies the runtime version and Ruby enum ABI against the header.
34
+ # @return [true]
35
+ # @raise [WGPU::Error] when differences are detected
30
36
  def verify!
31
37
  differences = enum_differences
38
+ version_difference = native_version_difference
39
+ differences.unshift(version_difference) if version_difference
32
40
  return true if differences.empty?
33
41
 
34
- raise WGPU::Error, "wgpu-native ABI enum differences:\n#{differences.join("\n")}"
42
+ raise WGPU::Error, "wgpu-native ABI differences:\n#{differences.join("\n")}"
35
43
  end
36
44
 
45
+ # Compares Ruby enum values with the pinned native header.
46
+ #
47
+ # @return [Array<String>] human-readable differences
37
48
  def enum_differences
38
49
  header_enums = parse_header
39
50
  ruby_enums.filter_map do |name, mapping|
@@ -44,10 +55,16 @@ module WGPU
44
55
  end
45
56
  end
46
57
 
58
+ # Locates the pinned header used for ABI verification.
59
+ # @return [String] absolute header path
60
+ # @raise [WGPU::Error] if no header is available
47
61
  def self.default_header_path
48
62
  override = ENV["WGPU_HEADER_PATH"]
49
63
  return File.expand_path(override) if override && !override.empty?
50
64
 
65
+ fixture = fixture_header_path
66
+ return fixture if File.file?(fixture)
67
+
51
68
  candidates = Distribution.cache_directories.map do |cache_dir|
52
69
  File.join(cache_dir, "include", "webgpu", "webgpu.h")
53
70
  end
@@ -56,13 +73,30 @@ module WGPU
56
73
 
57
74
  raise WGPU::Error, <<~MSG.chomp
58
75
  Pinned #{Distribution::VERSION} webgpu.h was not found.
59
- Run `bundle exec rake wgpu:install`, or set WGPU_HEADER_PATH.
60
- Searched: #{candidates.join(", ")}
76
+ Restore the repository ABI fixture, run `bundle exec rake wgpu:install`,
77
+ or set WGPU_HEADER_PATH.
78
+ Searched: #{([fixture] + candidates).join(", ")}
61
79
  MSG
62
80
  end
63
81
 
82
+ # Returns the repository's pinned enum fixture path.
83
+ #
84
+ # @return [String] absolute header fixture path
85
+ def self.fixture_header_path
86
+ File.expand_path("fixtures/webgpu-#{Distribution::VERSION}-enums.h", __dir__)
87
+ end
88
+
64
89
  private
65
90
 
91
+ def native_version_difference
92
+ actual = @native.wgpuGetVersion
93
+ expected = Distribution::ENCODED_VERSION
94
+ return if actual == expected
95
+
96
+ "version: runtime #{Distribution.version_string(actual)} (0x#{format("%08x", actual)}) " \
97
+ "does not match pinned #{Distribution::VERSION} (0x#{format("%08x", expected)})"
98
+ end
99
+
66
100
  def parse_header
67
101
  source = File.read(@header_path)
68
102
  source.scan(
@@ -8,6 +8,12 @@ module WGPU
8
8
  callback :request_device_callback,
9
9
  [RequestDeviceStatus, :pointer, StringView.by_value, :pointer, :pointer], :void
10
10
 
11
+ callback :create_compute_pipeline_async_callback,
12
+ [CreatePipelineAsyncStatus, :pointer, StringView.by_value, :pointer, :pointer], :void
13
+
14
+ callback :create_render_pipeline_async_callback,
15
+ [CreatePipelineAsyncStatus, :pointer, StringView.by_value, :pointer, :pointer], :void
16
+
11
17
  callback :buffer_map_callback,
12
18
  [MapAsyncStatus, StringView.by_value, :pointer, :pointer], :void
13
19