wgpu 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -0
  3. data/README.md +26 -3
  4. data/docs/README.md +22 -0
  5. data/docs/api_coverage.md +132 -0
  6. data/docs/async.md +31 -0
  7. data/docs/bind_groups.md +25 -0
  8. data/docs/buffer_data.md +37 -0
  9. data/docs/command_encoding.md +24 -0
  10. data/docs/errors.md +40 -0
  11. data/docs/getting_started_compute.md +61 -0
  12. data/docs/getting_started_rendering.md +62 -0
  13. data/docs/installation.md +94 -0
  14. data/docs/pipeline_descriptors.md +38 -0
  15. data/docs/releasing.md +22 -0
  16. data/docs/resource_lifetime.md +94 -0
  17. data/docs/shaders.md +32 -0
  18. data/docs/texture_readback.md +19 -0
  19. data/docs/troubleshooting.md +48 -0
  20. data/docs/upgrading_wgpu_native.md +26 -0
  21. data/ext/wgpu/extconf.rb +10 -142
  22. data/lib/wgpu/commands/command_buffer.rb +11 -0
  23. data/lib/wgpu/commands/command_encoder.rb +65 -8
  24. data/lib/wgpu/commands/compute_pass.rb +10 -0
  25. data/lib/wgpu/commands/render_bundle_encoder.rb +10 -3
  26. data/lib/wgpu/commands/render_pass.rb +34 -8
  27. data/lib/wgpu/core/adapter.rb +49 -20
  28. data/lib/wgpu/core/async_waiter.rb +92 -0
  29. data/lib/wgpu/core/canvas_context.rb +0 -2
  30. data/lib/wgpu/core/device.rb +160 -52
  31. data/lib/wgpu/core/instance.rb +9 -5
  32. data/lib/wgpu/core/queue.rb +81 -57
  33. data/lib/wgpu/core/surface.rb +16 -17
  34. data/lib/wgpu/data_types.rb +67 -0
  35. data/lib/wgpu/descriptor_helpers.rb +39 -0
  36. data/lib/wgpu/error.rb +55 -0
  37. data/lib/wgpu/logging.rb +63 -0
  38. data/lib/wgpu/native/abi_verifier.rb +109 -0
  39. data/lib/wgpu/native/callbacks.rb +9 -6
  40. data/lib/wgpu/native/capabilities.rb +31 -0
  41. data/lib/wgpu/native/distribution.rb +113 -0
  42. data/lib/wgpu/native/enum_helper.rb +63 -0
  43. data/lib/wgpu/native/enums.rb +82 -13
  44. data/lib/wgpu/native/functions.rb +17 -8
  45. data/lib/wgpu/native/installer.rb +192 -0
  46. data/lib/wgpu/native/loader.rb +40 -21
  47. data/lib/wgpu/native/structs.rb +19 -5
  48. data/lib/wgpu/native_resource.rb +171 -0
  49. data/lib/wgpu/pipeline/bind_group.rb +12 -0
  50. data/lib/wgpu/pipeline/bind_group_layout.rb +91 -37
  51. data/lib/wgpu/pipeline/compute_pipeline.rb +28 -26
  52. data/lib/wgpu/pipeline/render_pipeline.rb +180 -68
  53. data/lib/wgpu/pipeline/shader_module.rb +51 -12
  54. data/lib/wgpu/resources/buffer.rb +167 -84
  55. data/lib/wgpu/resources/query_set.rb +14 -12
  56. data/lib/wgpu/resources/sampler.rb +36 -20
  57. data/lib/wgpu/resources/texture.rb +44 -42
  58. data/lib/wgpu/resources/texture_view.rb +11 -3
  59. data/lib/wgpu/texture_format.rb +82 -0
  60. data/lib/wgpu/version.rb +1 -1
  61. data/lib/wgpu/window.rb +8 -1
  62. data/lib/wgpu.rb +33 -0
  63. data/sig/wgpu.rbs +381 -0
  64. metadata +33 -16
@@ -4,21 +4,26 @@ module WGPU
4
4
  class Device
5
5
  attr_reader :handle, :queue, :adapter
6
6
 
7
- CALLBACK_MODE_WAIT_ANY_ONLY = 1
8
7
  LIMIT_FIELDS = Native::Limits.members.freeze
9
8
 
10
- def self.request(adapter, label: nil, required_features: [], required_limits: nil)
9
+ def self.request(adapter, label: nil, required_features: [], required_limits: nil, timeout: nil)
11
10
  device_ptr = FFI::MemoryPointer.new(:pointer)
12
- status_holder = { value: nil, message: nil }
11
+ status_holder = { done: false, value: nil, message: nil }
12
+ device_callback_state = {
13
+ mutex: Mutex.new,
14
+ uncaptured_error: nil,
15
+ device_lost: nil
16
+ }
13
17
 
14
18
  callback = FFI::Function.new(
15
- :void, [:uint32, :pointer, Native::StringView.by_value, :pointer]
16
- ) do |status, device, message, _userdata|
19
+ :void, [:uint32, :pointer, Native::StringView.by_value, :pointer, :pointer]
20
+ ) do |status, device, message, _userdata1, _userdata2|
17
21
  status_holder[:value] = Native::RequestDeviceStatus[status]
18
22
  if message[:data] && !message[:data].null? && message[:length] > 0
19
23
  status_holder[:message] = message[:data].read_string(message[:length])
20
24
  end
21
25
  device_ptr.write_pointer(device)
26
+ status_holder[:done] = true
22
27
  end
23
28
 
24
29
  queue_desc = Native::QueueDescriptor.new
@@ -28,14 +33,18 @@ module WGPU
28
33
 
29
34
  device_lost_info = Native::DeviceLostCallbackInfo.new
30
35
  device_lost_info[:next_in_chain] = nil
31
- device_lost_info[:mode] = 0
32
- device_lost_info[:callback] = nil
33
- device_lost_info[:userdata] = nil
36
+ device_lost_info[:mode] = AsyncWaiter.callback_mode(instance: adapter.instance)
37
+ device_lost_callback = build_device_lost_callback(device_callback_state)
38
+ device_lost_info[:callback] = device_lost_callback
39
+ device_lost_info[:userdata1] = nil
40
+ device_lost_info[:userdata2] = nil
34
41
 
35
42
  error_info = Native::UncapturedErrorCallbackInfo.new
36
43
  error_info[:next_in_chain] = nil
37
- error_info[:callback] = nil
38
- error_info[:userdata] = nil
44
+ uncaptured_error_callback = build_uncaptured_error_callback(device_callback_state)
45
+ error_info[:callback] = uncaptured_error_callback
46
+ error_info[:userdata1] = nil
47
+ error_info[:userdata2] = nil
39
48
 
40
49
  desc = Native::DeviceDescriptor.new
41
50
  desc[:next_in_chain] = nil
@@ -67,11 +76,23 @@ module WGPU
67
76
 
68
77
  callback_info = Native::RequestDeviceCallbackInfo.new
69
78
  callback_info[:next_in_chain] = nil
70
- callback_info[:mode] = CALLBACK_MODE_WAIT_ANY_ONLY
79
+ callback_info[:mode] = AsyncWaiter.callback_mode(instance: adapter.instance)
71
80
  callback_info[:callback] = callback
72
- callback_info[:userdata] = nil
81
+ callback_info[:userdata1] = nil
82
+ callback_info[:userdata2] = nil
73
83
 
74
- Native.wgpuAdapterRequestDevice(adapter.handle, desc, callback_info)
84
+ callback_token = CallbackKeepalive.retain(adapter, callback)
85
+ begin
86
+ future = Native.wgpuAdapterRequestDevice(adapter.handle, desc, callback_info)
87
+ AsyncWaiter.wait(
88
+ status_holder: status_holder,
89
+ instance: adapter.instance,
90
+ future: future,
91
+ timeout: timeout
92
+ )
93
+ ensure
94
+ CallbackKeepalive.release(adapter, callback_token)
95
+ end
75
96
 
76
97
  handle = device_ptr.read_pointer
77
98
  if handle.null? || status_holder[:value] != :success
@@ -79,12 +100,22 @@ module WGPU
79
100
  raise DeviceError, "Failed to request device: #{msg}"
80
101
  end
81
102
 
82
- new(handle, adapter: adapter)
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
83
107
  end
84
108
 
85
- def initialize(handle, adapter: nil)
109
+ def initialize(handle, adapter: nil, label: nil, callback_state: nil)
86
110
  @handle = handle
87
111
  @adapter = adapter
112
+ @label = label
113
+ @device_callback_state = callback_state || {
114
+ mutex: Mutex.new,
115
+ uncaptured_error: nil,
116
+ device_lost: nil
117
+ }
118
+ @device_callback_tokens = []
88
119
  @queue = Queue.new(Native.wgpuDeviceGetQueue(@handle), device: self)
89
120
  end
90
121
 
@@ -96,8 +127,15 @@ module WGPU
96
127
  Buffer.new(self, label: label, size: size, usage: usage, mapped_at_creation: mapped_at_creation)
97
128
  end
98
129
 
99
- def create_shader_module(label: nil, code:, compilation_hints: [])
100
- ShaderModule.new(self, label: label, code: code, compilation_hints: compilation_hints)
130
+ def create_shader_module(label: nil, code: nil, spirv: nil, compilation_hints: [], validate: false)
131
+ ShaderModule.new(
132
+ self,
133
+ label: label,
134
+ code: code,
135
+ spirv: spirv,
136
+ compilation_hints: compilation_hints,
137
+ validate: validate
138
+ )
101
139
  end
102
140
 
103
141
  def create_command_encoder(label: nil)
@@ -181,8 +219,9 @@ module WGPU
181
219
  )
182
220
  end
183
221
 
184
- def create_buffer_with_data(label: nil, data:, usage:)
185
- data_ptr, byte_size = data_to_pointer(data)
222
+ def create_buffer_with_data(label: nil, data:, usage:, type: :f32)
223
+ data_ptr, byte_size = DataTypes.to_pointer(data, type:)
224
+ DataTypes.validate_alignment!(byte_size, 4, name: "buffer data size")
186
225
  buffer = create_buffer(
187
226
  label: label,
188
227
  size: byte_size,
@@ -235,19 +274,27 @@ module WGPU
235
274
  end
236
275
 
237
276
  def poll(wait: false)
238
- Native.wgpuDevicePoll(@handle, wait ? 1 : 0, nil)
277
+ if Native.device_poll_available?
278
+ Native.wgpuDevicePoll(@handle, wait ? 1 : 0, nil)
279
+ else
280
+ @adapter&.instance&.process_events
281
+ 0
282
+ end
239
283
  end
240
284
 
241
285
  def push_error_scope(filter = :validation)
242
- Native.wgpuDevicePushErrorScope(@handle, filter)
286
+ filter_value = Native::EnumHelper.coerce(Native::ErrorFilter, filter, name: "error filter")
287
+ Native.wgpuDevicePushErrorScope(@handle, filter_value)
243
288
  end
244
289
 
245
- def pop_error_scope
246
- error_holder = { type: nil, message: nil }
290
+ def pop_error_scope(timeout: nil)
291
+ error_holder = { done: false, status: nil, type: nil, message: nil }
247
292
 
248
293
  callback = FFI::Function.new(
249
294
  :void, [:uint32, :uint32, Native::StringView.by_value, :pointer, :pointer]
250
- ) do |_status, error_type, message, _userdata1, _userdata2|
295
+ ) do |status, error_type, message, _userdata1, _userdata2|
296
+ error_holder[:done] = true
297
+ error_holder[:status] = Native::PopErrorScopeStatus[status]
251
298
  error_holder[:type] = Native::ErrorType[error_type]
252
299
  if message[:data] && !message[:data].null? && message[:length] > 0
253
300
  error_holder[:message] = message[:data].read_string(message[:length])
@@ -256,30 +303,58 @@ module WGPU
256
303
 
257
304
  callback_info = Native::PopErrorScopeCallbackInfo.new
258
305
  callback_info[:next_in_chain] = nil
259
- callback_info[:mode] = 1
306
+ callback_info[:mode] = AsyncWaiter.callback_mode(instance: @adapter&.instance)
260
307
  callback_info[:callback] = callback
261
308
  callback_info[:userdata1] = nil
262
309
  callback_info[:userdata2] = nil
263
310
 
264
- Native.wgpuDevicePopErrorScope(@handle, callback_info)
311
+ 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
265
324
 
266
325
  error_holder
267
326
  end
268
327
 
269
- def pop_error_scope_async
270
- AsyncTask.new { pop_error_scope }
328
+ def pop_error_scope_async(timeout: nil)
329
+ AsyncTask.new { pop_error_scope(timeout: timeout) }
330
+ end
331
+
332
+ def pop_error_scope_typed(timeout: nil)
333
+ GPUError.from_hash(pop_error_scope(timeout: timeout))
271
334
  end
272
335
 
273
336
  def with_error_scope(filter = :validation)
274
337
  push_error_scope(filter)
275
338
  result = yield
276
- error = pop_error_scope
277
- if error[:type] && error[:type] != :no_error
278
- raise Error, "GPU error (#{error[:type]}): #{error[:message]}"
279
- end
339
+ error = GPUError.from_hash(pop_error_scope)
340
+ error&.raise!
280
341
  result
281
342
  end
282
343
 
344
+ def on_uncaptured_error(&handler)
345
+ raise ArgumentError, "on_uncaptured_error requires a block" unless handler
346
+
347
+ set_device_callback(:uncaptured_error, handler)
348
+ self
349
+ end
350
+
351
+ def on_device_lost(&handler)
352
+ raise ArgumentError, "on_device_lost requires a block" unless handler
353
+
354
+ set_device_callback(:device_lost, handler)
355
+ self
356
+ end
357
+
283
358
  def destroy
284
359
  return if @handle.null?
285
360
  Native.wgpuDeviceDestroy(@handle)
@@ -290,10 +365,10 @@ module WGPU
290
365
  return if @handle.null?
291
366
  Native.wgpuDeviceRelease(@handle)
292
367
  @handle = FFI::Pointer::NULL
368
+ Array(@device_callback_tokens).each { |token| CallbackKeepalive.release(self, token) }
369
+ @device_callback_tokens&.clear
293
370
  end
294
371
 
295
- private
296
-
297
372
  def self.normalize_required_features(required_features)
298
373
  Array(required_features).map do |feature|
299
374
  normalize_feature_name(feature)
@@ -304,10 +379,7 @@ module WGPU
304
379
  return feature if feature.is_a?(Integer)
305
380
 
306
381
  key = feature.to_s.strip.tr("-", "_").to_sym
307
- value = Native::FeatureName[key]
308
- raise ArgumentError, "Unknown feature name: #{feature}" if value.nil?
309
-
310
- value
382
+ Native::EnumHelper.coerce(Native::FeatureName, key, name: "feature name")
311
383
  end
312
384
 
313
385
  def self.build_required_limits(adapter, required_limits)
@@ -340,23 +412,59 @@ module WGPU
340
412
  raise ArgumentError, "Unknown limit key: #{name}"
341
413
  end
342
414
 
343
- def data_to_pointer(data)
344
- case data
345
- when String
346
- ptr = FFI::MemoryPointer.new(:char, data.bytesize)
347
- ptr.put_bytes(0, data)
348
- [ptr, data.bytesize]
349
- when Array
350
- ptr = FFI::MemoryPointer.new(:float, data.size)
351
- ptr.write_array_of_float(data)
352
- [ptr, data.size * 4]
353
- when FFI::Pointer
354
- [data, data.size]
355
- else
356
- raise ArgumentError, "Unsupported data type: #{data.class}"
415
+ def self.build_uncaptured_error_callback(state)
416
+ FFI::Function.new(
417
+ :void, [:pointer, :uint32, Native::StringView.by_value, :pointer, :pointer]
418
+ ) 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}"
425
+ end
357
426
  end
358
427
  end
359
428
 
429
+ def self.build_device_lost_callback(state)
430
+ FFI::Function.new(
431
+ :void, [:pointer, :uint32, Native::StringView.by_value, :pointer, :pointer]
432
+ ) 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
437
+ end
438
+ end
439
+ end
440
+
441
+ def self.dispatch_device_callback(state, key, *args)
442
+ handler = state[:mutex].synchronize { state[key] }
443
+ handler ? handler.call(*args) : yield
444
+ rescue StandardError => e
445
+ warn "WGPU #{key} handler failed: #{e.class}: #{e.message}"
446
+ end
447
+
448
+ def self.string_from_callback(message)
449
+ return "" if message[:data].nil? || message[:data].null? || message[:length].zero?
450
+
451
+ message[:data].read_string(message[:length])
452
+ end
453
+
454
+ private_class_method :normalize_required_features, :normalize_feature_name,
455
+ :build_required_limits, :canonical_limit_key, :build_uncaptured_error_callback,
456
+ :build_device_lost_callback, :dispatch_device_callback, :string_from_callback
457
+
458
+ private
459
+
460
+ def retain_device_callback(callback)
461
+ @device_callback_tokens << CallbackKeepalive.retain(self, callback)
462
+ end
463
+
464
+ def set_device_callback(name, handler)
465
+ @device_callback_state[:mutex].synchronize { @device_callback_state[name] = handler }
466
+ end
467
+
360
468
  def limits_to_hash(limits)
361
469
  {
362
470
  max_texture_dimension_1d: limits[:max_texture_dimension_1d],
@@ -15,25 +15,29 @@ module WGPU
15
15
  raise InitializationError, "Failed to create WebGPU instance" if @handle.null?
16
16
  end
17
17
 
18
- def request_adapter(power_preference: :high_performance, backend: nil, feature_level: :core, force_fallback_adapter: false, compatible_surface: nil)
18
+ def request_adapter(power_preference: :high_performance, backend: nil, feature_level: :core,
19
+ force_fallback_adapter: false, compatible_surface: nil, timeout: nil)
19
20
  Adapter.request(
20
21
  self,
21
22
  power_preference: power_preference,
22
23
  backend: backend,
23
24
  feature_level: feature_level,
24
25
  force_fallback_adapter: force_fallback_adapter,
25
- compatible_surface: compatible_surface
26
+ compatible_surface: compatible_surface,
27
+ timeout: timeout
26
28
  )
27
29
  end
28
30
 
29
- def request_adapter_async(power_preference: :high_performance, backend: nil, feature_level: :core, force_fallback_adapter: false, compatible_surface: nil)
31
+ def request_adapter_async(power_preference: :high_performance, backend: nil, feature_level: :core,
32
+ force_fallback_adapter: false, compatible_surface: nil, timeout: nil)
30
33
  AsyncTask.new do
31
34
  request_adapter(
32
35
  power_preference: power_preference,
33
36
  backend: backend,
34
37
  feature_level: feature_level,
35
38
  force_fallback_adapter: force_fallback_adapter,
36
- compatible_surface: compatible_surface
39
+ compatible_surface: compatible_surface,
40
+ timeout: timeout
37
41
  )
38
42
  end
39
43
  end
@@ -53,7 +57,7 @@ module WGPU
53
57
  Native.wgpuInstanceEnumerateAdapters(@handle, options, adapters_ptr)
54
58
 
55
59
  adapters_ptr.read_array_of_pointer(count).map do |ptr|
56
- Adapter.from_handle(ptr)
60
+ Adapter.from_handle(ptr, instance: self)
57
61
  end
58
62
  end
59
63
 
@@ -13,15 +13,24 @@ module WGPU
13
13
  buffers = Array(command_buffers)
14
14
  return if buffers.empty?
15
15
 
16
+ if buffers.map(&:object_id).uniq.length != buffers.length
17
+ raise CommandError, "The same command buffer cannot appear twice in one submission"
18
+ end
19
+ buffers.each do |buffer|
20
+ raise CommandError, "Command buffer has already been submitted" if buffer.submitted?
21
+ end
22
+
16
23
  handles = buffers.map(&:handle)
17
24
  ptr = FFI::MemoryPointer.new(:pointer, handles.size)
18
25
  ptr.write_array_of_pointer(handles)
19
26
 
20
27
  Native.wgpuQueueSubmit(@handle, handles.size, ptr)
28
+ buffers.each(&:mark_submitted!)
21
29
  end
22
30
 
23
- def write_buffer(buffer, buffer_offset, data, data_offset: 0, size: nil)
24
- data_ptr, byte_size = data_to_pointer(data)
31
+ def write_buffer(buffer, buffer_offset, data, data_offset: 0, size: nil, type: :f32)
32
+ data_ptr, byte_size = DataTypes.to_pointer(data, type:)
33
+ DataTypes.validate_alignment!(buffer_offset, 4, name: "buffer_offset")
25
34
  data_offset = Integer(data_offset)
26
35
  raise ArgumentError, "data_offset must be non-negative" if data_offset.negative?
27
36
  raise ArgumentError, "data_offset out of range" if data_offset > byte_size
@@ -29,6 +38,7 @@ module WGPU
29
38
  write_size = size.nil? ? (byte_size - data_offset) : Integer(size)
30
39
  raise ArgumentError, "size must be non-negative" if write_size.negative?
31
40
  raise ArgumentError, "data_offset + size out of range" if data_offset + write_size > byte_size
41
+ DataTypes.validate_alignment!(write_size, 4, name: "size")
32
42
 
33
43
  Native.wgpuQueueWriteBuffer(
34
44
  @handle,
@@ -39,8 +49,8 @@ module WGPU
39
49
  )
40
50
  end
41
51
 
42
- def write_texture(destination:, data:, data_layout:, size:)
43
- data_ptr, byte_size = data_to_pointer(data)
52
+ def write_texture(destination:, data:, data_layout:, size:, type: :f32)
53
+ data_ptr, byte_size = DataTypes.to_pointer(data, type:)
44
54
 
45
55
  dst = Native::ImageCopyTexture.new
46
56
  dst[:texture] = destination[:texture].handle
@@ -48,7 +58,11 @@ module WGPU
48
58
  dst[:origin][:x] = destination.dig(:origin, :x) || 0
49
59
  dst[:origin][:y] = destination.dig(:origin, :y) || 0
50
60
  dst[:origin][:z] = destination.dig(:origin, :z) || 0
51
- dst[:aspect] = destination[:aspect] || :all
61
+ dst[:aspect] = Native::EnumHelper.coerce(
62
+ Native::TextureAspect,
63
+ destination[:aspect] || :all,
64
+ name: "texture aspect"
65
+ )
52
66
 
53
67
  extent = Native::Extent3D.new
54
68
  if size.is_a?(Array)
@@ -69,14 +83,13 @@ module WGPU
69
83
  Native.wgpuQueueWriteTexture(@handle, dst, data_ptr, byte_size, layout, extent)
70
84
  end
71
85
 
72
- def read_buffer(buffer, offset: 0, size: nil, device:)
73
- size ||= buffer.size - offset
74
-
75
- staging = Buffer.new(device,
76
- size: size,
77
- usage: [:map_read, :copy_dst]
78
- )
86
+ def read_buffer(buffer, offset: 0, size: nil, device: nil, staging: nil)
87
+ device ||= @device
88
+ raise ArgumentError, "device is required when the queue has no owning device" unless device
79
89
 
90
+ size ||= buffer.size - offset
91
+ owns_staging = staging.nil?
92
+ staging ||= Buffer.new(device, size: size, usage: [:map_read, :copy_dst])
80
93
  encoder = CommandEncoder.new(device)
81
94
  encoder.copy_buffer_to_buffer(
82
95
  source: buffer,
@@ -89,26 +102,40 @@ module WGPU
89
102
  submit([command_buffer])
90
103
 
91
104
  staging.map_sync(:read)
92
- data = staging.read_mapped_data
93
- staging.unmap
94
- staging.release
95
-
96
- data
105
+ staging.read_mapped_data(size:)
106
+ ensure
107
+ staging&.unmap if staging && staging.map_state == :mapped
108
+ command_buffer&.release
109
+ encoder&.release
110
+ staging&.release if owns_staging
97
111
  end
98
112
 
99
- def read_texture(source:, data_layout:, size:, device:)
100
- width = size[:width] || size[0]
101
- height = size[:height] || size[1] || 1
102
- depth = size[:depth_or_array_layers] || size[2] || 1
113
+ def read_texture(source:, data_layout:, size:, device: nil, staging: nil)
114
+ device ||= @device
115
+ raise ArgumentError, "device is required when the queue has no owning device" unless device
116
+
117
+ width, height, depth = texture_extent(size)
103
118
  bytes_per_row = data_layout[:bytes_per_row]
104
- rows_per_image = data_layout[:rows_per_image] || height
105
- buffer_size = bytes_per_row * rows_per_image * depth
119
+ raise ArgumentError, "data_layout[:bytes_per_row] is required" unless bytes_per_row
106
120
 
107
- staging = Buffer.new(device,
108
- size: buffer_size,
109
- usage: [:map_read, :copy_dst]
121
+ DataTypes.validate_alignment!(
122
+ bytes_per_row,
123
+ TextureFormat::COPY_ALIGNMENT,
124
+ name: "bytes_per_row"
110
125
  )
126
+ format = source[:format] || source[:texture].format
127
+ aspect = source[:aspect] || :all
128
+ minimum_bytes_per_row = TextureFormat.bytes_per_row(width, format, aspect:)
129
+ if bytes_per_row < minimum_bytes_per_row
130
+ raise ArgumentError,
131
+ "bytes_per_row must be at least #{minimum_bytes_per_row} for width #{width} and #{format.inspect}"
132
+ end
133
+
134
+ rows_per_image = data_layout[:rows_per_image] || height
135
+ buffer_size = bytes_per_row * rows_per_image * depth
111
136
 
137
+ owns_staging = staging.nil?
138
+ staging ||= Buffer.new(device, size: buffer_size, usage: [:map_read, :copy_dst])
112
139
  encoder = CommandEncoder.new(device)
113
140
  encoder.copy_texture_to_buffer(
114
141
  source: source,
@@ -124,15 +151,17 @@ module WGPU
124
151
  submit([command_buffer])
125
152
 
126
153
  staging.map_sync(:read)
127
- data = staging.read_mapped_data
128
- staging.unmap
129
- staging.release
130
-
131
- data
154
+ staging.read_mapped_data(size: buffer_size)
155
+ ensure
156
+ staging&.unmap if staging && staging.map_state == :mapped
157
+ command_buffer&.release
158
+ encoder&.release
159
+ staging&.release if owns_staging
132
160
  end
133
161
 
134
- def on_submitted_work_done(device: nil)
162
+ def on_submitted_work_done(device: nil, timeout: nil)
135
163
  device ||= @device
164
+ instance = device&.adapter&.instance
136
165
  status_holder = { done: false, status: nil }
137
166
 
138
167
  callback = FFI::Function.new(
@@ -144,28 +173,31 @@ module WGPU
144
173
 
145
174
  callback_info = Native::QueueWorkDoneCallbackInfo.new
146
175
  callback_info[:next_in_chain] = nil
147
- callback_info[:mode] = 1
176
+ callback_info[:mode] = AsyncWaiter.callback_mode(instance: instance)
148
177
  callback_info[:callback] = callback
149
178
  callback_info[:userdata1] = nil
150
179
  callback_info[:userdata2] = nil
151
180
 
152
- Native.wgpuQueueOnSubmittedWorkDone(@handle, callback_info)
153
-
154
- if device
155
- until status_holder[:done]
156
- Native.wgpuDevicePoll(device.handle, 0, nil)
157
- sleep(0.001)
158
- end
159
- else
160
- sleep(0.001) until status_holder[:done]
181
+ 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)
161
193
  end
162
194
 
163
195
  status_holder[:status]
164
196
  end
165
197
 
166
- def on_submitted_work_done_async(device: nil)
198
+ def on_submitted_work_done_async(device: nil, timeout: nil)
167
199
  AsyncTask.new do
168
- on_submitted_work_done(device: device)
200
+ on_submitted_work_done(device: device, timeout: timeout)
169
201
  end
170
202
  end
171
203
 
@@ -177,21 +209,13 @@ module WGPU
177
209
 
178
210
  private
179
211
 
180
- def data_to_pointer(data)
181
- case data
182
- when String
183
- ptr = FFI::MemoryPointer.new(:char, data.bytesize)
184
- ptr.put_bytes(0, data)
185
- [ptr, data.bytesize]
186
- when Array
187
- ptr = FFI::MemoryPointer.new(:float, data.size)
188
- ptr.write_array_of_float(data)
189
- [ptr, data.size * 4]
190
- when FFI::Pointer
191
- [data, data.size]
212
+ def texture_extent(size)
213
+ if size.is_a?(Array)
214
+ [size.fetch(0), size[1] || 1, size[2] || 1]
192
215
  else
193
- raise ArgumentError, "Unsupported data type: #{data.class}"
216
+ [size.fetch(:width), size[:height] || 1, size[:depth_or_array_layers] || 1]
194
217
  end
195
218
  end
219
+
196
220
  end
197
221
  end
@@ -86,7 +86,7 @@ module WGPU
86
86
  config = Native::SurfaceConfiguration.new
87
87
  config[:next_in_chain] = nil
88
88
  config[:device] = device.handle
89
- config[:format] = format
89
+ config[:format] = Native::EnumHelper.coerce(Native::TextureFormat, format, name: "surface format")
90
90
  config[:usage] = normalize_usage(usage)
91
91
  config[:width] = width
92
92
  config[:height] = height
@@ -95,15 +95,23 @@ module WGPU
95
95
  @view_formats_ptr = nil
96
96
  config[:view_formats] = nil
97
97
  else
98
- format_values = view_formats.map do |vf|
99
- vf.is_a?(Integer) ? vf : Native::TextureFormat[vf]
98
+ format_values = view_formats.map do |view_format|
99
+ Native::EnumHelper.coerce(Native::TextureFormat, view_format, name: "view format")
100
100
  end
101
101
  @view_formats_ptr = FFI::MemoryPointer.new(:uint32, format_values.size)
102
102
  @view_formats_ptr.write_array_of_uint32(format_values)
103
103
  config[:view_formats] = @view_formats_ptr
104
104
  end
105
- config[:alpha_mode] = Native::CompositeAlphaMode[alpha_mode]
106
- config[:present_mode] = Native::PresentMode[present_mode]
105
+ config[:alpha_mode] = Native::EnumHelper.coerce(
106
+ Native::CompositeAlphaMode,
107
+ alpha_mode,
108
+ name: "alpha mode"
109
+ )
110
+ config[:present_mode] = Native::EnumHelper.coerce(
111
+ Native::PresentMode,
112
+ present_mode,
113
+ name: "present mode"
114
+ )
107
115
 
108
116
  Native.wgpuSurfaceConfigure(@handle, config)
109
117
  @configured = true
@@ -134,7 +142,7 @@ module WGPU
134
142
 
135
143
  status = Native::SurfaceGetCurrentTextureStatus[surface_texture[:status]]
136
144
  unless status == :success_optimal || status == :success_suboptimal
137
- raise SurfaceError, "Failed to get current texture: #{status}"
145
+ raise SurfaceAcquisitionError.new(status)
138
146
  end
139
147
 
140
148
  texture_ptr = surface_texture[:texture]
@@ -142,7 +150,7 @@ module WGPU
142
150
  raise SurfaceError, "Surface returned null texture"
143
151
  end
144
152
 
145
- Texture.from_handle(texture_ptr)
153
+ Texture.from_handle(texture_ptr, surface_status: status)
146
154
  end
147
155
 
148
156
  def get_current_texture
@@ -206,16 +214,7 @@ module WGPU
206
214
  private
207
215
 
208
216
  def normalize_usage(usage)
209
- case usage
210
- when Integer
211
- usage
212
- when Symbol
213
- Native::TextureUsage[usage]
214
- when Array
215
- usage.reduce(0) { |acc, u| acc | Native::TextureUsage[u] }
216
- else
217
- raise ArgumentError, "Invalid usage: #{usage}"
218
- end
217
+ Native::EnumHelper.coerce_flags(Native::TextureUsage, usage, name: "surface usage")
219
218
  end
220
219
  end
221
220
  end