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
@@ -6,24 +6,67 @@ module WGPU
6
6
 
7
7
  LIMIT_FIELDS = Native::Limits.members.freeze
8
8
 
9
+ # Requests a logical device and waits for the native callback.
10
+ #
11
+ # @param adapter [Adapter] adapter that will create the device
12
+ # @param label [String, nil] optional debugging label
13
+ # @param required_features [Array<Symbol, Integer>] features the device must enable
14
+ # @param required_limits [Hash, nil] minimum required limits
15
+ # @param timeout [Numeric, nil] maximum wait time in seconds
16
+ # @return [Device] requested device
17
+ # @raise [DeviceError] if device creation fails
18
+ # @raise [TimeoutError] if the request exceeds +timeout+
19
+ # @raise [ArgumentError] if +timeout+ is used without an instance-backed adapter
9
20
  def self.request(adapter, label: nil, required_features: [], required_limits: nil, timeout: nil)
21
+ if timeout && !adapter.instance
22
+ raise ArgumentError,
23
+ "Device.request timeout requires an instance-backed adapter; " \
24
+ "pass instance: to Adapter.from_handle"
25
+ end
26
+
10
27
  device_ptr = FFI::MemoryPointer.new(:pointer)
11
- status_holder = { done: false, value: nil, message: nil }
28
+ status_holder = {
29
+ done: false,
30
+ value: nil,
31
+ message: nil,
32
+ abandoned: false,
33
+ cleanup_claimed: false,
34
+ mutex: Mutex.new
35
+ }
12
36
  device_callback_state = {
13
37
  mutex: Mutex.new,
14
38
  uncaptured_error: nil,
15
39
  device_lost: nil
16
40
  }
17
41
 
42
+ callback_token = nil
18
43
  callback = FFI::Function.new(
19
44
  :void, [:uint32, :pointer, Native::StringView.by_value, :pointer, :pointer]
20
45
  ) do |status, device, message, _userdata1, _userdata2|
21
- status_holder[:value] = Native::RequestDeviceStatus[status]
22
- if message[:data] && !message[:data].null? && message[:length] > 0
23
- status_holder[:message] = message[:data].read_string(message[:length])
46
+ cleanup_abandoned = false
47
+ begin
48
+ status_holder[:mutex].synchronize do
49
+ status_holder[:value] = Native::RequestDeviceStatus[status]
50
+ if message[:data] && !message[:data].null? && message[:length] > 0
51
+ status_holder[:message] = message[:data].read_string(message[:length])
52
+ end
53
+ device_ptr.write_pointer(device)
54
+ status_holder[:done] = true
55
+ if status_holder[:abandoned] && !status_holder[:cleanup_claimed]
56
+ status_holder[:cleanup_claimed] = true
57
+ cleanup_abandoned = true
58
+ end
59
+ end
60
+ if cleanup_abandoned
61
+ if status_holder[:value] == :success && device && !device.null?
62
+ release_abandoned_device(device, device_callback_state)
63
+ else
64
+ release_device_callback_keepalive(device_callback_state)
65
+ end
66
+ end
67
+ ensure
68
+ CallbackKeepalive.release(adapter, callback_token)
24
69
  end
25
- device_ptr.write_pointer(device)
26
- status_holder[:done] = true
27
70
  end
28
71
 
29
72
  queue_desc = Native::QueueDescriptor.new
@@ -81,35 +124,89 @@ module WGPU
81
124
  callback_info[:userdata1] = nil
82
125
  callback_info[:userdata2] = nil
83
126
 
127
+ device_lost_token = CallbackKeepalive.retain(adapter, device_lost_callback)
128
+ uncaptured_error_token = CallbackKeepalive.retain(adapter, uncaptured_error_callback)
129
+ configure_device_callback_keepalive(
130
+ device_callback_state,
131
+ owner: adapter,
132
+ tokens: [device_lost_token, uncaptured_error_token]
133
+ )
84
134
  callback_token = CallbackKeepalive.retain(adapter, callback)
135
+ future =
136
+ begin
137
+ Native.wgpuAdapterRequestDevice(adapter.handle, desc, callback_info)
138
+ rescue StandardError
139
+ CallbackKeepalive.release(adapter, callback_token)
140
+ release_device_callback_keepalive(device_callback_state)
141
+ raise
142
+ end
143
+
85
144
  begin
86
- future = Native.wgpuAdapterRequestDevice(adapter.handle, desc, callback_info)
87
145
  AsyncWaiter.wait(
88
146
  status_holder: status_holder,
89
147
  instance: adapter.instance,
90
148
  future: future,
91
149
  timeout: timeout
92
150
  )
93
- ensure
94
- CallbackKeepalive.release(adapter, callback_token)
151
+ rescue TimeoutError
152
+ abandoned_device = nil
153
+ cleanup_abandoned = status_holder[:mutex].synchronize do
154
+ status_holder[:abandoned] = true
155
+ next false unless status_holder[:done] && !status_holder[:cleanup_claimed]
156
+
157
+ status_holder[:cleanup_claimed] = true
158
+ abandoned_device = device_ptr.read_pointer
159
+ true
160
+ end
161
+ if cleanup_abandoned
162
+ if status_holder[:value] == :success && abandoned_device && !abandoned_device.null?
163
+ release_abandoned_device(abandoned_device, device_callback_state)
164
+ else
165
+ release_device_callback_keepalive(device_callback_state)
166
+ end
167
+ end
168
+ raise
95
169
  end
96
170
 
97
171
  handle = device_ptr.read_pointer
98
172
  if handle.null? || status_holder[:value] != :success
173
+ release_device_callback_keepalive(device_callback_state)
99
174
  msg = status_holder[:message] || "Unknown error"
100
175
  raise DeviceError, "Failed to request device: #{msg}"
101
176
  end
102
177
 
103
- device = new(handle, adapter: adapter, label: label, callback_state: device_callback_state)
104
- device.send(:retain_device_callback, device_lost_callback)
105
- device.send(:retain_device_callback, uncaptured_error_callback)
106
- device
178
+ begin
179
+ callback_lifetime = DeviceCallbackLifetime.new do
180
+ release_device_callback_keepalive(device_callback_state)
181
+ end
182
+ device_callback_state[:mutex].synchronize do
183
+ device_callback_state[:callback_lifetime] = callback_lifetime
184
+ end
185
+ device = new(
186
+ handle,
187
+ adapter: adapter,
188
+ label: label,
189
+ callback_state: device_callback_state,
190
+ callback_lifetime: callback_lifetime
191
+ )
192
+ device.send(:adopt_device_callback_keepalive)
193
+ device
194
+ rescue StandardError
195
+ device ? device.release : release_abandoned_device(handle, device_callback_state)
196
+ raise
197
+ end
107
198
  end
108
199
 
109
- def initialize(handle, adapter: nil, label: nil, callback_state: nil)
200
+ # Wraps a native device and obtains its default queue.
201
+ # @param handle [FFI::Pointer] native device handle
202
+ # @param adapter [Adapter, nil] adapter that created the device
203
+ # @param label [String, nil] optional debug label
204
+ # @param callback_lifetime [DeviceCallbackLifetime, nil] shared native callback lifetime
205
+ def initialize(handle, adapter: nil, label: nil, callback_state: nil, callback_lifetime: nil)
110
206
  @handle = handle
111
207
  @adapter = adapter
112
208
  @label = label
209
+ @device_callback_lifetime = callback_lifetime
113
210
  @device_callback_state = callback_state || {
114
211
  mutex: Mutex.new,
115
212
  uncaptured_error: nil,
@@ -119,14 +216,20 @@ module WGPU
119
216
  @queue = Queue.new(Native.wgpuDeviceGetQueue(@handle), device: self)
120
217
  end
121
218
 
219
+ # Returns information about the adapter that created this device.
220
+ # @return [Hash, nil]
122
221
  def adapter_info
123
222
  @adapter&.info
124
223
  end
125
224
 
225
+ # Creates a GPU buffer.
226
+ # @return [Buffer]
126
227
  def create_buffer(label: nil, size:, usage:, mapped_at_creation: false)
127
228
  Buffer.new(self, label: label, size: size, usage: usage, mapped_at_creation: mapped_at_creation)
128
229
  end
129
230
 
231
+ # Creates a shader module from WGSL or SPIR-V source.
232
+ # @return [ShaderModule]
130
233
  def create_shader_module(label: nil, code: nil, spirv: nil, compilation_hints: [], validate: false)
131
234
  ShaderModule.new(
132
235
  self,
@@ -138,32 +241,46 @@ module WGPU
138
241
  )
139
242
  end
140
243
 
244
+ # Creates a command encoder.
245
+ # @return [CommandEncoder]
141
246
  def create_command_encoder(label: nil)
142
247
  CommandEncoder.new(self, label: label)
143
248
  end
144
249
 
250
+ # Creates a bind group layout.
251
+ # @return [BindGroupLayout]
145
252
  def create_bind_group_layout(label: nil, entries:)
146
253
  BindGroupLayout.new(self, label: label, entries: entries)
147
254
  end
148
255
 
256
+ # Creates a bind group.
257
+ # @return [BindGroup]
149
258
  def create_bind_group(label: nil, layout:, entries:)
150
259
  BindGroup.new(self, label: label, layout: layout, entries: entries)
151
260
  end
152
261
 
262
+ # Creates a pipeline layout.
263
+ # @return [PipelineLayout]
153
264
  def create_pipeline_layout(label: nil, bind_group_layouts:)
154
265
  PipelineLayout.new(self, label: label, bind_group_layouts: bind_group_layouts)
155
266
  end
156
267
 
268
+ # Creates a compute pipeline.
269
+ # @return [ComputePipeline]
157
270
  def create_compute_pipeline(label: nil, layout:, compute:)
158
271
  ComputePipeline.new(self, label: label, layout: layout, compute: compute)
159
272
  end
160
273
 
274
+ # Creates a compute pipeline on a background task.
275
+ # @return [AsyncTask] task yielding a {ComputePipeline}
161
276
  def create_compute_pipeline_async(label: nil, layout:, compute:)
162
277
  AsyncTask.new do
163
278
  create_compute_pipeline(label: label, layout: layout, compute: compute)
164
279
  end
165
280
  end
166
281
 
282
+ # Creates a render pipeline.
283
+ # @return [RenderPipeline]
167
284
  def create_render_pipeline(label: nil, layout:, vertex:, primitive: {}, depth_stencil: nil, multisample: {}, fragment: nil)
168
285
  RenderPipeline.new(self,
169
286
  label: label,
@@ -176,6 +293,8 @@ module WGPU
176
293
  )
177
294
  end
178
295
 
296
+ # Creates a render pipeline on a background task.
297
+ # @return [AsyncTask] task yielding a {RenderPipeline}
179
298
  def create_render_pipeline_async(label: nil, layout:, vertex:, primitive: {}, depth_stencil: nil, multisample: {}, fragment: nil)
180
299
  AsyncTask.new do
181
300
  create_render_pipeline(
@@ -190,6 +309,8 @@ module WGPU
190
309
  end
191
310
  end
192
311
 
312
+ # Creates a texture.
313
+ # @return [Texture]
193
314
  def create_texture(label: nil, size:, format:, usage:, dimension: :d2, mip_level_count: 1, sample_count: 1, view_formats: [])
194
315
  Texture.new(self,
195
316
  label: label,
@@ -203,6 +324,8 @@ module WGPU
203
324
  )
204
325
  end
205
326
 
327
+ # Creates a texture sampler.
328
+ # @return [Sampler]
206
329
  def create_sampler(label: nil, address_mode_u: :clamp_to_edge, address_mode_v: :clamp_to_edge, address_mode_w: :clamp_to_edge, mag_filter: :nearest, min_filter: :nearest, mipmap_filter: :nearest, lod_min_clamp: 0.0, lod_max_clamp: 32.0, compare: nil, max_anisotropy: 1)
207
330
  Sampler.new(self,
208
331
  label: label,
@@ -219,6 +342,11 @@ module WGPU
219
342
  )
220
343
  end
221
344
 
345
+ # Creates a mapped buffer initialized from typed Ruby data.
346
+ # @param data [Array, String, FFI::Pointer] source data
347
+ # @param usage [Symbol, Array<Symbol>, Integer] buffer usage flags
348
+ # @param type [Symbol] source element type
349
+ # @return [Buffer]
222
350
  def create_buffer_with_data(label: nil, data:, usage:, type: :f32)
223
351
  data_ptr, byte_size = DataTypes.to_pointer(data, type:)
224
352
  DataTypes.validate_alignment!(byte_size, 4, name: "buffer data size")
@@ -233,10 +361,14 @@ module WGPU
233
361
  buffer
234
362
  end
235
363
 
364
+ # Creates a GPU query set.
365
+ # @return [QuerySet]
236
366
  def create_query_set(label: nil, type:, count:)
237
367
  QuerySet.new(self, label: label, type: type, count: count)
238
368
  end
239
369
 
370
+ # Creates an encoder for reusable render commands.
371
+ # @return [RenderBundleEncoder]
240
372
  def create_render_bundle_encoder(color_formats:, depth_stencil_format: nil, sample_count: 1,
241
373
  depth_read_only: false, stencil_read_only: false, label: nil)
242
374
  RenderBundleEncoder.new(self,
@@ -249,6 +381,8 @@ module WGPU
249
381
  )
250
382
  end
251
383
 
384
+ # Lists optional features enabled on the device.
385
+ # @return [Array<Symbol>]
252
386
  def features
253
387
  supported = Native::SupportedFeatures.new
254
388
  Native.wgpuDeviceGetFeatures(@handle, supported)
@@ -262,10 +396,15 @@ module WGPU
262
396
  result
263
397
  end
264
398
 
399
+ # Reports whether a feature is enabled on the device.
400
+ # @param feature [Symbol] feature name
401
+ # @return [Boolean]
265
402
  def has_feature?(feature)
266
403
  features.include?(feature)
267
404
  end
268
405
 
406
+ # Returns resource limits supported by the device.
407
+ # @return [Hash{Symbol => Integer}]
269
408
  def limits
270
409
  supported = Native::SupportedLimits.new
271
410
  supported[:next_in_chain] = nil
@@ -273,6 +412,9 @@ module WGPU
273
412
  limits_to_hash(supported[:limits])
274
413
  end
275
414
 
415
+ # Processes device work, optionally waiting for queue progress.
416
+ # @param wait [Boolean] whether to wait for submitted work
417
+ # @return [Integer] native poll status
276
418
  def poll(wait: false)
277
419
  if Native.device_poll_available?
278
420
  Native.wgpuDevicePoll(@handle, wait ? 1 : 0, nil)
@@ -282,22 +424,35 @@ module WGPU
282
424
  end
283
425
  end
284
426
 
427
+ # Pushes a scope that captures matching GPU errors.
428
+ # @param filter [Symbol, Integer] error filter
429
+ # @return [void]
285
430
  def push_error_scope(filter = :validation)
286
431
  filter_value = Native::EnumHelper.coerce(Native::ErrorFilter, filter, name: "error filter")
287
432
  Native.wgpuDevicePushErrorScope(@handle, filter_value)
288
433
  end
289
434
 
435
+ # Pops the latest error scope and waits for its result.
436
+ # @param timeout [Numeric, nil] maximum wait time in seconds
437
+ # @return [Hash] native status, error type, and message
290
438
  def pop_error_scope(timeout: nil)
291
439
  error_holder = { done: false, status: nil, type: nil, message: nil }
292
440
 
441
+ callback_lifetime_release = device_callback_lifetime_lease
442
+ callback_token = nil
293
443
  callback = FFI::Function.new(
294
444
  :void, [:uint32, :uint32, Native::StringView.by_value, :pointer, :pointer]
295
445
  ) do |status, error_type, message, _userdata1, _userdata2|
296
- error_holder[:done] = true
297
- error_holder[:status] = Native::PopErrorScopeStatus[status]
298
- error_holder[:type] = Native::ErrorType[error_type]
299
- if message[:data] && !message[:data].null? && message[:length] > 0
300
- error_holder[:message] = message[:data].read_string(message[:length])
446
+ begin
447
+ error_holder[:status] = Native::PopErrorScopeStatus[status]
448
+ error_holder[:type] = Native::ErrorType[error_type]
449
+ if message[:data] && !message[:data].null? && message[:length] > 0
450
+ error_holder[:message] = message[:data].read_string(message[:length])
451
+ end
452
+ error_holder[:done] = true
453
+ ensure
454
+ CallbackKeepalive.release(self, callback_token)
455
+ callback_lifetime_release.call
301
456
  end
302
457
  end
303
458
 
@@ -309,30 +464,44 @@ module WGPU
309
464
  callback_info[:userdata2] = nil
310
465
 
311
466
  callback_token = CallbackKeepalive.retain(self, callback)
312
- begin
313
- future = Native.wgpuDevicePopErrorScope(@handle, callback_info)
314
- AsyncWaiter.wait(
315
- status_holder: error_holder,
316
- instance: @adapter&.instance,
317
- device: self,
318
- future: future,
319
- timeout: timeout
320
- )
321
- ensure
322
- CallbackKeepalive.release(self, callback_token)
323
- end
467
+ future =
468
+ begin
469
+ Native.wgpuDevicePopErrorScope(@handle, callback_info)
470
+ rescue StandardError
471
+ CallbackKeepalive.release(self, callback_token)
472
+ callback_lifetime_release.call
473
+ raise
474
+ end
475
+ AsyncWaiter.wait(
476
+ status_holder: error_holder,
477
+ instance: @adapter&.instance,
478
+ device: self,
479
+ future: future,
480
+ timeout: timeout
481
+ )
324
482
 
325
483
  error_holder
326
484
  end
327
485
 
486
+ # Pops the latest error scope on a background thread.
487
+ #
488
+ # @param timeout [Numeric, nil] maximum wait time in seconds
489
+ # @return [AsyncTask] task whose value is the error hash
328
490
  def pop_error_scope_async(timeout: nil)
329
491
  AsyncTask.new { pop_error_scope(timeout: timeout) }
330
492
  end
331
493
 
494
+ # Pops the latest error scope as a typed error.
495
+ # @param timeout [Numeric, nil] maximum wait time in seconds
496
+ # @return [GPUError, nil]
332
497
  def pop_error_scope_typed(timeout: nil)
333
498
  GPUError.from_hash(pop_error_scope(timeout: timeout))
334
499
  end
335
500
 
501
+ # Runs a block inside an error scope and raises captured GPU errors.
502
+ # @param filter [Symbol, Integer] error filter
503
+ # @yieldreturn [Object] block result
504
+ # @return [Object] block result
336
505
  def with_error_scope(filter = :validation)
337
506
  push_error_scope(filter)
338
507
  result = yield
@@ -341,6 +510,10 @@ module WGPU
341
510
  result
342
511
  end
343
512
 
513
+ # Registers the handler for uncaptured GPU errors.
514
+ # @yieldparam error [GPUError] reported error
515
+ # @return [Device] this device
516
+ # @raise [ArgumentError] if no block is provided
344
517
  def on_uncaptured_error(&handler)
345
518
  raise ArgumentError, "on_uncaptured_error requires a block" unless handler
346
519
 
@@ -348,6 +521,11 @@ module WGPU
348
521
  self
349
522
  end
350
523
 
524
+ # Registers the handler invoked when the device is lost.
525
+ # @yieldparam reason [Symbol] native device-lost reason
526
+ # @yieldparam message [String] native diagnostic message
527
+ # @return [Device] this device
528
+ # @raise [ArgumentError] if no block is provided
351
529
  def on_device_lost(&handler)
352
530
  raise ArgumentError, "on_device_lost requires a block" unless handler
353
531
 
@@ -355,18 +533,26 @@ module WGPU
355
533
  self
356
534
  end
357
535
 
536
+ # Destroys device-owned native resources.
537
+ # @return [void]
358
538
  def destroy
359
539
  return if @handle.null?
360
540
  Native.wgpuDeviceDestroy(@handle)
361
541
  end
362
542
 
543
+ # Releases the default queue and native device handle.
544
+ #
545
+ # Device callbacks remain alive until all derived wrappers and pending
546
+ # callback operations have also completed their native releases.
547
+ #
548
+ # Calling this method more than once has no effect.
549
+ # @return [void]
363
550
  def release
364
551
  @queue&.release
365
552
  return if @handle.null?
366
553
  Native.wgpuDeviceRelease(@handle)
367
554
  @handle = FFI::Pointer::NULL
368
- Array(@device_callback_tokens).each { |token| CallbackKeepalive.release(self, token) }
369
- @device_callback_tokens&.clear
555
+ @device_callback_tokens = nil
370
556
  end
371
557
 
372
558
  def self.normalize_required_features(required_features)
@@ -416,12 +602,17 @@ module WGPU
416
602
  FFI::Function.new(
417
603
  :void, [:pointer, :uint32, Native::StringView.by_value, :pointer, :pointer]
418
604
  ) do |_device, type, message, _userdata1, _userdata2|
419
- error = GPUError.new(
420
- type: Native::ErrorType[type] || :unknown,
421
- message: string_from_callback(message)
422
- )
423
- dispatch_device_callback(state, :uncaptured_error, error) do
424
- warn "Uncaptured GPU error (#{error.type}): #{error.message}"
605
+ callback_lifetime = retain_device_callback_lifetime(state)
606
+ begin
607
+ error = GPUError.new(
608
+ type: Native::ErrorType[type] || :unknown,
609
+ message: string_from_callback(message)
610
+ )
611
+ dispatch_device_callback(state, :uncaptured_error, error) do
612
+ warn "Uncaptured GPU error (#{error.type}): #{error.message}"
613
+ end
614
+ ensure
615
+ callback_lifetime&.release
425
616
  end
426
617
  end
427
618
  end
@@ -430,12 +621,67 @@ module WGPU
430
621
  FFI::Function.new(
431
622
  :void, [:pointer, :uint32, Native::StringView.by_value, :pointer, :pointer]
432
623
  ) do |_device, reason, message, _userdata1, _userdata2|
433
- reason_name = Native::DeviceLostReason[reason] || :unknown
434
- message_text = string_from_callback(message)
435
- dispatch_device_callback(state, :device_lost, reason_name, message_text) do
436
- warn "GPU device lost (#{reason_name}): #{message_text}" unless reason_name == :destroyed
624
+ callback_lifetime = retain_device_callback_lifetime(state)
625
+ begin
626
+ reason_name = Native::DeviceLostReason[reason] || :unknown
627
+ message_text = string_from_callback(message)
628
+ dispatch_device_callback(state, :device_lost, reason_name, message_text) do
629
+ warn "GPU device lost (#{reason_name}): #{message_text}" unless reason_name == :destroyed
630
+ end
631
+ ensure
632
+ callback_lifetime&.release
633
+ end
634
+ end
635
+ end
636
+
637
+ def self.retain_device_callback_lifetime(state)
638
+ state[:mutex].synchronize do
639
+ lifetime = state[:callback_lifetime]
640
+ lifetime&.retain
641
+ lifetime
642
+ end
643
+ end
644
+
645
+ def self.configure_device_callback_keepalive(state, owner:, tokens:)
646
+ state[:mutex].synchronize do
647
+ state[:callback_owner] = owner
648
+ state[:callback_tokens] = tokens
649
+ state[:callbacks_completed] = false
650
+ end
651
+ end
652
+
653
+ def self.transfer_device_callback_keepalive(state, new_owner)
654
+ state[:mutex].synchronize do
655
+ return [] if state[:callbacks_completed]
656
+
657
+ previous_owner = state[:callback_owner]
658
+ tokens = Array(state[:callback_tokens])
659
+ transferred = tokens.select do |token|
660
+ CallbackKeepalive.transfer(previous_owner, new_owner, token)
437
661
  end
662
+ state[:callback_owner] = new_owner
663
+ state[:callback_tokens] = transferred
664
+ transferred
665
+ end
666
+ end
667
+
668
+ def self.release_device_callback_keepalive(state)
669
+ owner, tokens = state[:mutex].synchronize do
670
+ return if state[:callbacks_completed]
671
+
672
+ state[:callbacks_completed] = true
673
+ retained_owner = state.delete(:callback_owner)
674
+ retained_tokens = Array(state.delete(:callback_tokens))
675
+ state.delete(:callback_lifetime)
676
+ [retained_owner, retained_tokens]
438
677
  end
678
+ tokens.each { |token| CallbackKeepalive.release(owner, token) } if owner
679
+ end
680
+
681
+ def self.release_abandoned_device(device, callback_state)
682
+ Native.wgpuDeviceRelease(device)
683
+ ensure
684
+ release_device_callback_keepalive(callback_state)
439
685
  end
440
686
 
441
687
  def self.dispatch_device_callback(state, key, *args)
@@ -453,12 +699,16 @@ module WGPU
453
699
 
454
700
  private_class_method :normalize_required_features, :normalize_feature_name,
455
701
  :build_required_limits, :canonical_limit_key, :build_uncaptured_error_callback,
456
- :build_device_lost_callback, :dispatch_device_callback, :string_from_callback
702
+ :build_device_lost_callback, :retain_device_callback_lifetime,
703
+ :configure_device_callback_keepalive,
704
+ :transfer_device_callback_keepalive, :release_device_callback_keepalive,
705
+ :release_abandoned_device, :dispatch_device_callback, :string_from_callback
457
706
 
458
707
  private
459
708
 
460
- def retain_device_callback(callback)
461
- @device_callback_tokens << CallbackKeepalive.retain(self, callback)
709
+ def adopt_device_callback_keepalive
710
+ @device_callback_tokens =
711
+ self.class.send(:transfer_device_callback_keepalive, @device_callback_state, self)
462
712
  end
463
713
 
464
714
  def set_device_callback(name, handler)
@@ -4,6 +4,8 @@ module WGPU
4
4
  class Instance
5
5
  attr_reader :handle
6
6
 
7
+ # Creates a WebGPU instance.
8
+ # @raise [InitializationError] if native instance creation fails
7
9
  def initialize
8
10
  desc = Native::InstanceDescriptor.new
9
11
  desc[:next_in_chain] = nil
@@ -15,6 +17,8 @@ module WGPU
15
17
  raise InitializationError, "Failed to create WebGPU instance" if @handle.null?
16
18
  end
17
19
 
20
+ # Requests an adapter matching the supplied preferences.
21
+ # @return [Adapter]
18
22
  def request_adapter(power_preference: :high_performance, backend: nil, feature_level: :core,
19
23
  force_fallback_adapter: false, compatible_surface: nil, timeout: nil)
20
24
  Adapter.request(
@@ -28,6 +32,8 @@ module WGPU
28
32
  )
29
33
  end
30
34
 
35
+ # Requests an adapter on a background task.
36
+ # @return [AsyncTask] task yielding an {Adapter}
31
37
  def request_adapter_async(power_preference: :high_performance, backend: nil, feature_level: :core,
32
38
  force_fallback_adapter: false, compatible_surface: nil, timeout: nil)
33
39
  AsyncTask.new do
@@ -42,6 +48,9 @@ module WGPU
42
48
  end
43
49
  end
44
50
 
51
+ # Lists adapters exposed by the instance.
52
+ # @param backends [Integer, nil] backend bit mask
53
+ # @return [Array<Adapter>]
45
54
  def enumerate_adapters(backends: nil)
46
55
  options = nil
47
56
  if backends
@@ -61,20 +70,31 @@ module WGPU
61
70
  end
62
71
  end
63
72
 
73
+ # Enumerates adapters on a background task.
74
+ # @return [AsyncTask] task yielding adapter objects
64
75
  def enumerate_adapters_async(backends: nil)
65
76
  AsyncTask.new do
66
77
  enumerate_adapters(backends: backends)
67
78
  end
68
79
  end
69
80
 
81
+ # Creates a canvas context from platform presentation information.
82
+ # @param present_info [Hash] platform surface information
83
+ # @return [CanvasContext]
70
84
  def get_canvas_context(present_info)
71
85
  CanvasContext.new(self, present_info)
72
86
  end
73
87
 
88
+ # Processes pending instance callbacks and events.
89
+ # @return [void]
74
90
  def process_events
75
91
  Native.wgpuInstanceProcessEvents(@handle)
76
92
  end
77
93
 
94
+ # Releases the native instance handle.
95
+ #
96
+ # Calling this method more than once has no effect.
97
+ # @return [void]
78
98
  def release
79
99
  return if @handle.null?
80
100
  Native.wgpuInstanceRelease(@handle)