wgpu 1.1.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 (63) 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 +36 -11
  28. data/lib/wgpu/core/async_waiter.rb +32 -4
  29. data/lib/wgpu/core/canvas_context.rb +0 -2
  30. data/lib/wgpu/core/device.rb +138 -42
  31. data/lib/wgpu/core/instance.rb +8 -4
  32. data/lib/wgpu/core/queue.rb +80 -49
  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 +4 -1
  40. data/lib/wgpu/native/capabilities.rb +18 -2
  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 +60 -8
  44. data/lib/wgpu/native/functions.rb +10 -10
  45. data/lib/wgpu/native/installer.rb +192 -0
  46. data/lib/wgpu/native/loader.rb +39 -21
  47. data/lib/wgpu/native_resource.rb +171 -0
  48. data/lib/wgpu/pipeline/bind_group.rb +12 -0
  49. data/lib/wgpu/pipeline/bind_group_layout.rb +91 -37
  50. data/lib/wgpu/pipeline/compute_pipeline.rb +28 -26
  51. data/lib/wgpu/pipeline/render_pipeline.rb +180 -68
  52. data/lib/wgpu/pipeline/shader_module.rb +50 -8
  53. data/lib/wgpu/resources/buffer.rb +148 -73
  54. data/lib/wgpu/resources/query_set.rb +14 -12
  55. data/lib/wgpu/resources/sampler.rb +36 -20
  56. data/lib/wgpu/resources/texture.rb +44 -42
  57. data/lib/wgpu/resources/texture_view.rb +11 -3
  58. data/lib/wgpu/texture_format.rb +82 -0
  59. data/lib/wgpu/version.rb +1 -1
  60. data/lib/wgpu/window.rb +8 -1
  61. data/lib/wgpu.rb +32 -0
  62. data/sig/wgpu.rbs +381 -0
  63. metadata +31 -16
@@ -6,9 +6,14 @@ module WGPU
6
6
 
7
7
  LIMIT_FIELDS = Native::Limits.members.freeze
8
8
 
9
- 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)
10
10
  device_ptr = FFI::MemoryPointer.new(:pointer)
11
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
+ }
12
17
 
13
18
  callback = FFI::Function.new(
14
19
  :void, [:uint32, :pointer, Native::StringView.by_value, :pointer, :pointer]
@@ -28,14 +33,16 @@ 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
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
33
39
  device_lost_info[:userdata1] = nil
34
40
  device_lost_info[:userdata2] = nil
35
41
 
36
42
  error_info = Native::UncapturedErrorCallbackInfo.new
37
43
  error_info[:next_in_chain] = nil
38
- error_info[:callback] = nil
44
+ uncaptured_error_callback = build_uncaptured_error_callback(device_callback_state)
45
+ error_info[:callback] = uncaptured_error_callback
39
46
  error_info[:userdata1] = nil
40
47
  error_info[:userdata2] = nil
41
48
 
@@ -74,8 +81,18 @@ module WGPU
74
81
  callback_info[:userdata1] = nil
75
82
  callback_info[:userdata2] = nil
76
83
 
77
- future = Native.wgpuAdapterRequestDevice(adapter.handle, desc, callback_info)
78
- AsyncWaiter.wait(status_holder: status_holder, instance: adapter.instance, future: future)
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
79
96
 
80
97
  handle = device_ptr.read_pointer
81
98
  if handle.null? || status_holder[:value] != :success
@@ -83,12 +100,22 @@ module WGPU
83
100
  raise DeviceError, "Failed to request device: #{msg}"
84
101
  end
85
102
 
86
- 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
87
107
  end
88
108
 
89
- def initialize(handle, adapter: nil)
109
+ def initialize(handle, adapter: nil, label: nil, callback_state: nil)
90
110
  @handle = handle
91
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 = []
92
119
  @queue = Queue.new(Native.wgpuDeviceGetQueue(@handle), device: self)
93
120
  end
94
121
 
@@ -100,8 +127,15 @@ module WGPU
100
127
  Buffer.new(self, label: label, size: size, usage: usage, mapped_at_creation: mapped_at_creation)
101
128
  end
102
129
 
103
- def create_shader_module(label: nil, code:, compilation_hints: [])
104
- 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
+ )
105
139
  end
106
140
 
107
141
  def create_command_encoder(label: nil)
@@ -185,8 +219,9 @@ module WGPU
185
219
  )
186
220
  end
187
221
 
188
- def create_buffer_with_data(label: nil, data:, usage:)
189
- 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")
190
225
  buffer = create_buffer(
191
226
  label: label,
192
227
  size: byte_size,
@@ -248,10 +283,11 @@ module WGPU
248
283
  end
249
284
 
250
285
  def push_error_scope(filter = :validation)
251
- Native.wgpuDevicePushErrorScope(@handle, filter)
286
+ filter_value = Native::EnumHelper.coerce(Native::ErrorFilter, filter, name: "error filter")
287
+ Native.wgpuDevicePushErrorScope(@handle, filter_value)
252
288
  end
253
289
 
254
- def pop_error_scope
290
+ def pop_error_scope(timeout: nil)
255
291
  error_holder = { done: false, status: nil, type: nil, message: nil }
256
292
 
257
293
  callback = FFI::Function.new(
@@ -272,26 +308,53 @@ module WGPU
272
308
  callback_info[:userdata1] = nil
273
309
  callback_info[:userdata2] = nil
274
310
 
275
- future = Native.wgpuDevicePopErrorScope(@handle, callback_info)
276
- AsyncWaiter.wait(status_holder: error_holder, instance: @adapter&.instance, device: self, future: future)
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
277
324
 
278
325
  error_holder
279
326
  end
280
327
 
281
- def pop_error_scope_async
282
- 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))
283
334
  end
284
335
 
285
336
  def with_error_scope(filter = :validation)
286
337
  push_error_scope(filter)
287
338
  result = yield
288
- error = pop_error_scope
289
- if error[:type] && error[:type] != :no_error
290
- raise Error, "GPU error (#{error[:type]}): #{error[:message]}"
291
- end
339
+ error = GPUError.from_hash(pop_error_scope)
340
+ error&.raise!
292
341
  result
293
342
  end
294
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
+
295
358
  def destroy
296
359
  return if @handle.null?
297
360
  Native.wgpuDeviceDestroy(@handle)
@@ -302,10 +365,10 @@ module WGPU
302
365
  return if @handle.null?
303
366
  Native.wgpuDeviceRelease(@handle)
304
367
  @handle = FFI::Pointer::NULL
368
+ Array(@device_callback_tokens).each { |token| CallbackKeepalive.release(self, token) }
369
+ @device_callback_tokens&.clear
305
370
  end
306
371
 
307
- private
308
-
309
372
  def self.normalize_required_features(required_features)
310
373
  Array(required_features).map do |feature|
311
374
  normalize_feature_name(feature)
@@ -316,10 +379,7 @@ module WGPU
316
379
  return feature if feature.is_a?(Integer)
317
380
 
318
381
  key = feature.to_s.strip.tr("-", "_").to_sym
319
- value = Native::FeatureName[key]
320
- raise ArgumentError, "Unknown feature name: #{feature}" if value.nil?
321
-
322
- value
382
+ Native::EnumHelper.coerce(Native::FeatureName, key, name: "feature name")
323
383
  end
324
384
 
325
385
  def self.build_required_limits(adapter, required_limits)
@@ -352,23 +412,59 @@ module WGPU
352
412
  raise ArgumentError, "Unknown limit key: #{name}"
353
413
  end
354
414
 
355
- def data_to_pointer(data)
356
- case data
357
- when String
358
- ptr = FFI::MemoryPointer.new(:char, data.bytesize)
359
- ptr.put_bytes(0, data)
360
- [ptr, data.bytesize]
361
- when Array
362
- ptr = FFI::MemoryPointer.new(:float, data.size)
363
- ptr.write_array_of_float(data)
364
- [ptr, data.size * 4]
365
- when FFI::Pointer
366
- [data, data.size]
367
- else
368
- 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
426
+ end
427
+ end
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
369
438
  end
370
439
  end
371
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
+
372
468
  def limits_to_hash(limits)
373
469
  {
374
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
@@ -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
111
133
 
134
+ rows_per_image = data_layout[:rows_per_image] || height
135
+ buffer_size = bytes_per_row * rows_per_image * depth
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,14 +151,15 @@ 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
136
164
  instance = device&.adapter&.instance
137
165
  status_holder = { done: false, status: nil }
@@ -150,15 +178,26 @@ module WGPU
150
178
  callback_info[:userdata1] = nil
151
179
  callback_info[:userdata2] = nil
152
180
 
153
- future = Native.wgpuQueueOnSubmittedWorkDone(@handle, callback_info)
154
- AsyncWaiter.wait(status_holder: status_holder, instance: instance, device: device, future: future)
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)
193
+ end
155
194
 
156
195
  status_holder[:status]
157
196
  end
158
197
 
159
- def on_submitted_work_done_async(device: nil)
198
+ def on_submitted_work_done_async(device: nil, timeout: nil)
160
199
  AsyncTask.new do
161
- on_submitted_work_done(device: device)
200
+ on_submitted_work_done(device: device, timeout: timeout)
162
201
  end
163
202
  end
164
203
 
@@ -170,21 +209,13 @@ module WGPU
170
209
 
171
210
  private
172
211
 
173
- def data_to_pointer(data)
174
- case data
175
- when String
176
- ptr = FFI::MemoryPointer.new(:char, data.bytesize)
177
- ptr.put_bytes(0, data)
178
- [ptr, data.bytesize]
179
- when Array
180
- ptr = FFI::MemoryPointer.new(:float, data.size)
181
- ptr.write_array_of_float(data)
182
- [ptr, data.size * 4]
183
- when FFI::Pointer
184
- [data, data.size]
212
+ def texture_extent(size)
213
+ if size.is_a?(Array)
214
+ [size.fetch(0), size[1] || 1, size[2] || 1]
185
215
  else
186
- raise ArgumentError, "Unsupported data type: #{data.class}"
216
+ [size.fetch(:width), size[:height] || 1, size[:depth_or_array_layers] || 1]
187
217
  end
188
218
  end
219
+
189
220
  end
190
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
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WGPU
4
+ module DataTypes
5
+ FORMATS = {
6
+ f32: ["e*", 4],
7
+ f64: ["E*", 8],
8
+ u32: ["L<*", 4],
9
+ i32: ["l<*", 4],
10
+ u16: ["S<*", 2],
11
+ u8: ["C*", 1]
12
+ }.freeze
13
+
14
+ module_function
15
+
16
+ def pack(values, type: :f32)
17
+ format, = format_for(type)
18
+ Array(values).pack(format)
19
+ rescue RangeError => e
20
+ raise ArgumentError, "value is out of range for #{type}: #{e.message}"
21
+ end
22
+
23
+ def unpack(bytes, type: :f32)
24
+ format, byte_size = format_for(type)
25
+ unless (bytes.bytesize % byte_size).zero?
26
+ raise ArgumentError, "#{type} data size must be a multiple of #{byte_size} bytes"
27
+ end
28
+
29
+ bytes.unpack(format)
30
+ end
31
+
32
+ def byte_size(type)
33
+ format_for(type).last
34
+ end
35
+
36
+ def to_pointer(data, type: :f32)
37
+ return [data, pointer_size(data)] if data.is_a?(FFI::Pointer)
38
+
39
+ bytes = data.is_a?(String) ? data : pack(data, type:)
40
+ pointer = FFI::MemoryPointer.new(:char, bytes.bytesize)
41
+ pointer.put_bytes(0, bytes)
42
+ [pointer, bytes.bytesize]
43
+ end
44
+
45
+ def validate_alignment!(value, alignment, name:)
46
+ integer = Integer(value)
47
+ raise ArgumentError, "#{name} must be non-negative" if integer.negative?
48
+ return integer if (integer % alignment).zero?
49
+
50
+ raise ArgumentError, "#{name} must be aligned to #{alignment} bytes (got #{integer})"
51
+ end
52
+
53
+ def format_for(type)
54
+ FORMATS.fetch(type.to_sym)
55
+ rescue NoMethodError, KeyError
56
+ raise ArgumentError, "Unknown data type #{type.inspect}. Valid values: #{FORMATS.keys.map(&:inspect).join(", ")}"
57
+ end
58
+ private_class_method :format_for
59
+
60
+ def pointer_size(pointer)
61
+ pointer.size
62
+ rescue NoMethodError
63
+ raise ArgumentError, "FFI pointer must have a known size; pass size explicitly"
64
+ end
65
+ private_class_method :pointer_size
66
+ end
67
+ end