wgpu 1.2.1 → 1.2.2

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.
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "monitor"
4
+
3
5
  module WGPU
4
6
  class Device
5
7
  attr_reader :handle, :queue, :adapter
@@ -18,6 +20,7 @@ module WGPU
18
20
  # @raise [TimeoutError] if the request exceeds +timeout+
19
21
  # @raise [ArgumentError] if +timeout+ is used without an instance-backed adapter
20
22
  def self.request(adapter, label: nil, required_features: [], required_limits: nil, timeout: nil)
23
+ timeout = AsyncWaiter.normalize_timeout(timeout)
21
24
  if timeout && !adapter.instance
22
25
  raise ArgumentError,
23
26
  "Device.request timeout requires an instance-backed adapter; " \
@@ -134,7 +137,7 @@ module WGPU
134
137
  callback_token = CallbackKeepalive.retain(adapter, callback)
135
138
  future =
136
139
  begin
137
- Native.wgpuAdapterRequestDevice(adapter.handle, desc, callback_info)
140
+ Native.wgpuAdapterRequestDevice(NativeResource.checked_handle(adapter, expected_class: Adapter), desc, callback_info)
138
141
  rescue StandardError
139
142
  CallbackKeepalive.release(adapter, callback_token)
140
143
  release_device_callback_keepalive(device_callback_state)
@@ -148,7 +151,7 @@ module WGPU
148
151
  future: future,
149
152
  timeout: timeout
150
153
  )
151
- rescue TimeoutError
154
+ rescue StandardError
152
155
  abandoned_device = nil
153
156
  cleanup_abandoned = status_holder[:mutex].synchronize do
154
157
  status_holder[:abandoned] = true
@@ -192,7 +195,8 @@ module WGPU
192
195
  device.send(:adopt_device_callback_keepalive)
193
196
  device
194
197
  rescue StandardError
195
- device ? device.release : release_abandoned_device(handle, device_callback_state)
198
+ device&.release
199
+ release_device_callback_keepalive(device_callback_state)
196
200
  raise
197
201
  end
198
202
  end
@@ -394,6 +398,8 @@ module WGPU
394
398
  end
395
399
  end
396
400
  result
401
+ ensure
402
+ Native.wgpuSupportedFeaturesFreeMembers(supported) if supported
397
403
  end
398
404
 
399
405
  # Reports whether a feature is enabled on the device.
@@ -425,70 +431,41 @@ module WGPU
425
431
  end
426
432
 
427
433
  # Pushes a scope that captures matching GPU errors.
434
+ # Scopes belong to the pushing thread and serialize scoped work on this device.
435
+ # Pop on the same thread; do not wait here for another thread's scoped work.
428
436
  # @param filter [Symbol, Integer] error filter
429
437
  # @return [void]
430
438
  def push_error_scope(filter = :validation)
431
439
  filter_value = Native::EnumHelper.coerce(Native::ErrorFilter, filter, name: "error filter")
432
- Native.wgpuDevicePushErrorScope(@handle, filter_value)
440
+ error_scope_monitor.enter
441
+ begin
442
+ ensure_not_released!
443
+ Native.wgpuDevicePushErrorScope(@handle, filter_value)
444
+ @error_scope_depth = (@error_scope_depth || 0) + 1
445
+ @error_scope_owner = Thread.current
446
+ rescue StandardError
447
+ error_scope_monitor.exit
448
+ raise
449
+ end
433
450
  end
434
451
 
435
452
  # Pops the latest error scope and waits for its result.
436
453
  # @param timeout [Numeric, nil] maximum wait time in seconds
437
454
  # @return [Hash] native status, error type, and message
438
455
  def pop_error_scope(timeout: nil)
439
- error_holder = { done: false, status: nil, type: nil, message: nil }
440
-
441
- callback_lifetime_release = device_callback_lifetime_lease
442
- callback_token = nil
443
- callback = FFI::Function.new(
444
- :void, [:uint32, :uint32, Native::StringView.by_value, :pointer, :pointer]
445
- ) do |status, error_type, message, _userdata1, _userdata2|
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
456
- end
457
- end
458
-
459
- callback_info = Native::PopErrorScopeCallbackInfo.new
460
- callback_info[:next_in_chain] = nil
461
- callback_info[:mode] = AsyncWaiter.callback_mode(instance: @adapter&.instance)
462
- callback_info[:callback] = callback
463
- callback_info[:userdata1] = nil
464
- callback_info[:userdata2] = nil
465
-
466
- callback_token = CallbackKeepalive.retain(self, callback)
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
- )
482
-
483
- error_holder
456
+ timeout = AsyncWaiter.normalize_timeout(timeout)
457
+ error_holder, future = begin_pop_error_scope
458
+ wait_for_error_scope(error_holder, future, timeout: timeout)
484
459
  end
485
460
 
486
- # Pops the latest error scope on a background thread.
461
+ # Starts popping on the calling thread and waits for completion in a background task.
487
462
  #
488
463
  # @param timeout [Numeric, nil] maximum wait time in seconds
489
464
  # @return [AsyncTask] task whose value is the error hash
490
465
  def pop_error_scope_async(timeout: nil)
491
- AsyncTask.new { pop_error_scope(timeout: timeout) }
466
+ timeout = AsyncWaiter.normalize_timeout(timeout)
467
+ error_holder, future = begin_pop_error_scope
468
+ AsyncTask.new { wait_for_error_scope(error_holder, future, timeout: timeout) }
492
469
  end
493
470
 
494
471
  # Pops the latest error scope as a typed error.
@@ -503,10 +480,11 @@ module WGPU
503
480
  # @yieldreturn [Object] block result
504
481
  # @return [Object] block result
505
482
  def with_error_scope(filter = :validation)
506
- push_error_scope(filter)
507
- result = yield
508
- error = GPUError.from_hash(pop_error_scope)
509
- error&.raise!
483
+ raise ArgumentError, "block is required" unless block_given?
484
+
485
+ result = nil
486
+ error = capture_error_scope(filter) { result = yield }
487
+ GPUError.from_hash(error)&.raise!
510
488
  result
511
489
  end
512
490
 
@@ -706,6 +684,85 @@ module WGPU
706
684
 
707
685
  private
708
686
 
687
+ def begin_pop_error_scope
688
+ unless @error_scope_owner == Thread.current && @error_scope_depth.to_i.positive?
689
+ raise DeviceError, "No error scope belongs to the current thread"
690
+ end
691
+
692
+ error_holder = { done: false, status: nil, type: nil, message: nil }
693
+
694
+ callback_lifetime_release = device_callback_lifetime_lease
695
+ callback_token = nil
696
+ callback = FFI::Function.new(
697
+ :void, [:uint32, :uint32, Native::StringView.by_value, :pointer, :pointer]
698
+ ) do |status, error_type, message, _userdata1, _userdata2|
699
+ begin
700
+ error_holder[:status] = Native::PopErrorScopeStatus[status]
701
+ error_holder[:type] = Native::ErrorType[error_type]
702
+ if message[:data] && !message[:data].null? && message[:length] > 0
703
+ error_holder[:message] = message[:data].read_string(message[:length])
704
+ end
705
+ error_holder[:done] = true
706
+ ensure
707
+ CallbackKeepalive.release(self, callback_token)
708
+ callback_lifetime_release.call
709
+ end
710
+ end
711
+
712
+ callback_info = Native::PopErrorScopeCallbackInfo.new
713
+ callback_info[:next_in_chain] = nil
714
+ callback_info[:mode] = AsyncWaiter.callback_mode(instance: @adapter&.instance)
715
+ callback_info[:callback] = callback
716
+ callback_info[:userdata1] = nil
717
+ callback_info[:userdata2] = nil
718
+
719
+ callback_token = CallbackKeepalive.retain(self, callback)
720
+ @error_scope_depth -= 1
721
+ @error_scope_owner = nil if @error_scope_depth.zero?
722
+ future =
723
+ begin
724
+ Native.wgpuDevicePopErrorScope(@handle, callback_info)
725
+ rescue StandardError
726
+ CallbackKeepalive.release(self, callback_token)
727
+ callback_lifetime_release.call
728
+ raise
729
+ ensure
730
+ error_scope_monitor.exit
731
+ end
732
+ [error_holder, future]
733
+ end
734
+
735
+ def wait_for_error_scope(error_holder, future, timeout:)
736
+ AsyncWaiter.wait(
737
+ status_holder: error_holder,
738
+ instance: @adapter&.instance,
739
+ device: self,
740
+ future: future,
741
+ timeout: timeout
742
+ )
743
+ error_holder
744
+ end
745
+
746
+ def error_scope_monitor
747
+ @error_scope_monitor ||= Monitor.new
748
+ end
749
+
750
+ # The reentrant scope lock spans push, the operation, and native pop.
751
+ def capture_error_scope(filter = :validation)
752
+ push_error_scope(filter)
753
+ begin
754
+ yield
755
+ ensure
756
+ active_error = $!
757
+ begin
758
+ error = pop_error_scope
759
+ rescue StandardError
760
+ raise unless active_error
761
+ end
762
+ end
763
+ error
764
+ end
765
+
709
766
  def adopt_device_callback_keepalive
710
767
  @device_callback_tokens =
711
768
  self.class.send(:transfer_device_callback_keepalive, @device_callback_state, self)
@@ -27,7 +27,7 @@ module WGPU
27
27
  raise CommandError, "Command buffer has already been submitted" if buffer.submitted?
28
28
  end
29
29
 
30
- handles = buffers.map(&:handle)
30
+ handles = buffers.map { |buffer| NativeResource.checked_handle(buffer, expected_class: CommandBuffer) }
31
31
  ptr = FFI::MemoryPointer.new(:pointer, handles.size)
32
32
  ptr.write_array_of_pointer(handles)
33
33
 
@@ -54,7 +54,7 @@ module WGPU
54
54
 
55
55
  Native.wgpuQueueWriteBuffer(
56
56
  @handle,
57
- buffer.handle,
57
+ NativeResource.checked_handle(buffer, expected_class: Buffer),
58
58
  buffer_offset,
59
59
  data_ptr + data_offset,
60
60
  write_size
@@ -71,7 +71,7 @@ module WGPU
71
71
  data_ptr, byte_size = DataTypes.to_pointer(data, type:)
72
72
 
73
73
  dst = Native::ImageCopyTexture.new
74
- dst[:texture] = destination[:texture].handle
74
+ dst[:texture] = NativeResource.checked_handle(destination[:texture], expected_class: Texture)
75
75
  dst[:mip_level] = destination[:mip_level] || 0
76
76
  dst[:origin][:x] = destination.dig(:origin, :x) || 0
77
77
  dst[:origin][:y] = destination.dig(:origin, :y) || 0
@@ -82,16 +82,7 @@ module WGPU
82
82
  name: "texture aspect"
83
83
  )
84
84
 
85
- extent = Native::Extent3D.new
86
- if size.is_a?(Array)
87
- extent[:width] = size[0]
88
- extent[:height] = size[1] || 1
89
- extent[:depth_or_array_layers] = size[2] || 1
90
- else
91
- extent[:width] = size[:width]
92
- extent[:height] = size[:height] || 1
93
- extent[:depth_or_array_layers] = size[:depth_or_array_layers] || 1
94
- end
85
+ extent = DescriptorHelpers.extent_3d(size)
95
86
 
96
87
  layout = Native::TextureDataLayout.new
97
88
  layout[:offset] = data_layout[:offset] || 0
@@ -148,7 +139,8 @@ module WGPU
148
139
  device ||= @device
149
140
  raise ArgumentError, "device is required when the queue has no owning device" unless device
150
141
 
151
- width, height, depth = texture_extent(size)
142
+ extent = DescriptorHelpers.extent_3d(size)
143
+ width, height, depth = extent.values
152
144
  bytes_per_row = data_layout[:bytes_per_row]
153
145
  raise ArgumentError, "data_layout[:bytes_per_row] is required" unless bytes_per_row
154
146
 
@@ -206,6 +198,7 @@ module WGPU
206
198
  # @param timeout [Numeric, nil] maximum wait time in seconds
207
199
  # @return [Symbol] native completion status
208
200
  def on_submitted_work_done(device: nil, timeout: nil)
201
+ timeout = AsyncWaiter.normalize_timeout(timeout)
209
202
  device ||= @device
210
203
  instance = device&.adapter&.instance
211
204
  status_holder = { done: false, status: nil }
@@ -271,14 +264,6 @@ module WGPU
271
264
 
272
265
  private
273
266
 
274
- def texture_extent(size)
275
- if size.is_a?(Array)
276
- [size.fetch(0), size[1] || 1, size[2] || 1]
277
- else
278
- [size.fetch(:width), size[:height] || 1, size[:depth_or_array_layers] || 1]
279
- end
280
- end
281
-
282
267
  def validate_readback_staging!(staging, required_size)
283
268
  if staging.size < required_size
284
269
  raise ArgumentError,
@@ -18,7 +18,7 @@ module WGPU
18
18
  desc[:label][:data] = nil
19
19
  desc[:label][:length] = 0
20
20
 
21
- handle = Native.wgpuInstanceCreateSurface(instance.handle, desc)
21
+ handle = Native.wgpuInstanceCreateSurface(NativeResource.checked_handle(instance, expected_class: Instance), desc)
22
22
  raise SurfaceError, "Failed to create surface from Metal layer" if handle.null?
23
23
 
24
24
  new(handle, instance)
@@ -39,7 +39,7 @@ module WGPU
39
39
  desc[:label][:data] = nil
40
40
  desc[:label][:length] = 0
41
41
 
42
- handle = Native.wgpuInstanceCreateSurface(instance.handle, desc)
42
+ handle = Native.wgpuInstanceCreateSurface(NativeResource.checked_handle(instance, expected_class: Instance), desc)
43
43
  raise SurfaceError, "Failed to create surface from Windows HWND" if handle.null?
44
44
 
45
45
  new(handle, instance)
@@ -60,7 +60,7 @@ module WGPU
60
60
  desc[:label][:data] = nil
61
61
  desc[:label][:length] = 0
62
62
 
63
- handle = Native.wgpuInstanceCreateSurface(instance.handle, desc)
63
+ handle = Native.wgpuInstanceCreateSurface(NativeResource.checked_handle(instance, expected_class: Instance), desc)
64
64
  raise SurfaceError, "Failed to create surface from Xlib window" if handle.null?
65
65
 
66
66
  new(handle, instance)
@@ -81,7 +81,7 @@ module WGPU
81
81
  desc[:label][:data] = nil
82
82
  desc[:label][:length] = 0
83
83
 
84
- handle = Native.wgpuInstanceCreateSurface(instance.handle, desc)
84
+ handle = Native.wgpuInstanceCreateSurface(NativeResource.checked_handle(instance, expected_class: Instance), desc)
85
85
  raise SurfaceError, "Failed to create surface from Wayland surface" if handle.null?
86
86
 
87
87
  new(handle, instance)
@@ -102,7 +102,7 @@ module WGPU
102
102
  def configure(device:, format:, usage: :render_attachment, width:, height:, present_mode: :fifo, alpha_mode: :auto, view_formats: [])
103
103
  config = Native::SurfaceConfiguration.new
104
104
  config[:next_in_chain] = nil
105
- config[:device] = device.handle
105
+ config[:device] = NativeResource.checked_handle(device, expected_class: Device)
106
106
  config[:format] = Native::EnumHelper.coerce(Native::TextureFormat, format, name: "surface format")
107
107
  config[:usage] = normalize_usage(usage)
108
108
  config[:width] = width
@@ -216,7 +216,7 @@ module WGPU
216
216
  # @return [Hash]
217
217
  def capabilities(adapter)
218
218
  caps = Native::SurfaceCapabilities.new
219
- Native.wgpuSurfaceGetCapabilities(@handle, adapter.handle, caps)
219
+ Native.wgpuSurfaceGetCapabilities(@handle, NativeResource.checked_handle(adapter, expected_class: Adapter), caps)
220
220
 
221
221
  formats = []
222
222
  if caps[:format_count] > 0 && !caps[:formats].null?
@@ -6,6 +6,28 @@ module WGPU
6
6
 
7
7
  module_function
8
8
 
9
+ # Normalizes an Array or Hash texture extent to a native descriptor.
10
+ # @param size [Array<Integer>, Hash] width, height, and depth/layer count
11
+ # @return [Native::Extent3D]
12
+ def extent_3d(size)
13
+ values = case size
14
+ when Array
15
+ [size.fetch(0), size[1] || 1, size[2] || 1]
16
+ when Hash
17
+ [size.fetch(:width), size[:height] || 1, size[:depth_or_array_layers] || 1]
18
+ else
19
+ raise ArgumentError, "texture size must be an Array or Hash"
20
+ end
21
+ extent = Native::Extent3D.new
22
+ [:width, :height, :depth_or_array_layers].zip(values).each do |field, value|
23
+ value = Integer(value)
24
+ raise ArgumentError, "#{field} must be between 0 and 4294967295" unless (0..0xFFFFFFFF).cover?(value)
25
+
26
+ extent[field] = value
27
+ end
28
+ extent
29
+ end
30
+
9
31
  # Writes an optional Ruby label into a native descriptor.
10
32
  #
11
33
  # @param descriptor [FFI::Struct] descriptor with a +label+ member
@@ -292,6 +292,9 @@ module WGPU
292
292
  attach_function :wgpuTextureViewRelease,
293
293
  [:pointer], :void
294
294
 
295
+ attach_function :wgpuSupportedFeaturesFreeMembers,
296
+ [SupportedFeatures.by_value], :void
297
+
295
298
  attach_function :wgpuAdapterInfoFreeMembers,
296
299
  [AdapterInfo.by_value], :void
297
300
 
@@ -186,7 +186,13 @@ module WGPU
186
186
  end
187
187
 
188
188
  FileUtils.mkdir_p(entry.directory? ? target : File.dirname(target))
189
- entry.extract(target) { true } unless entry.directory?
189
+ next if entry.directory?
190
+
191
+ if entry.method(:extract).parameters.include?([:key, :destination_directory])
192
+ entry.extract(entry.name, destination_directory: destination) { true }
193
+ else
194
+ entry.extract(target) { true }
195
+ end
190
196
  end
191
197
  end
192
198
  true
@@ -182,6 +182,15 @@ module WGPU
182
182
  module NativeResource
183
183
  GUARDED_METHOD_EXEMPTIONS = [:initialize, :release, :released?, :handle, :label, :inspect].freeze
184
184
 
185
+ # Validates resource arguments before passing their handles to FFI.
186
+ def self.checked_handle(resource, expected_class:)
187
+ unless resource.is_a?(expected_class)
188
+ raise TypeError, "expected #{expected_class}, got #{resource.class}"
189
+ end
190
+ resource.send(:ensure_not_released!)
191
+ resource.handle
192
+ end
193
+
185
194
  module ClassMethods
186
195
  private
187
196
 
@@ -205,6 +214,9 @@ module WGPU
205
214
  super
206
215
  initialize_native_resource(label: kwargs.fetch(:label, UNSET))
207
216
  attach_device_callback_lifetime_from_parent
217
+ initialized = true
218
+ ensure
219
+ release if !initialized && @handle && !released?
208
220
  end
209
221
 
210
222
  # Releases a resource once and unregisters it from leak tracking.
@@ -284,6 +296,10 @@ module WGPU
284
296
 
285
297
  private
286
298
 
299
+ def initialize_copy(_source)
300
+ raise TypeError, "#{self.class} owns a native resource and cannot be copied"
301
+ end
302
+
287
303
  def attach_device_callback_lifetime(owner)
288
304
  lifetime = owner&.instance_variable_get(:@device_callback_lifetime)
289
305
  return self unless lifetime
@@ -29,13 +29,13 @@ module WGPU
29
29
  desc[:label][:data] = nil
30
30
  desc[:label][:length] = 0
31
31
  end
32
- desc[:layout] = layout.handle
32
+ desc[:layout] = NativeResource.checked_handle(layout, expected_class: BindGroupLayout)
33
33
  desc[:entry_count] = entries_array.size
34
34
  desc[:entries] = entries_ptr
35
35
 
36
- device.push_error_scope(:validation)
37
- @handle = Native.wgpuDeviceCreateBindGroup(device.handle, desc)
38
- error = device.pop_error_scope
36
+ error = device.send(:capture_error_scope) do
37
+ @handle = Native.wgpuDeviceCreateBindGroup(NativeResource.checked_handle(device, expected_class: Device), desc)
38
+ end
39
39
 
40
40
  if @handle.null? || (error[:type] && error[:type] != :no_error)
41
41
  msg = error[:message] || "Failed to create bind group"
@@ -74,7 +74,7 @@ module WGPU
74
74
 
75
75
  if entry_hash[:buffer]
76
76
  buffer = entry_hash[:buffer]
77
- entry[:buffer] = buffer.handle
77
+ entry[:buffer] = NativeResource.checked_handle(buffer, expected_class: Buffer)
78
78
  entry[:offset] = entry_hash[:offset] || 0
79
79
  entry[:size] = entry_hash[:size] || buffer.size
80
80
  else
@@ -84,13 +84,13 @@ module WGPU
84
84
  end
85
85
 
86
86
  if entry_hash[:sampler]
87
- entry[:sampler] = entry_hash[:sampler].handle
87
+ entry[:sampler] = NativeResource.checked_handle(entry_hash[:sampler], expected_class: Sampler)
88
88
  else
89
89
  entry[:sampler] = nil
90
90
  end
91
91
 
92
92
  if entry_hash[:texture_view]
93
- entry[:texture_view] = entry_hash[:texture_view].handle
93
+ entry[:texture_view] = NativeResource.checked_handle(entry_hash[:texture_view], expected_class: TextureView)
94
94
  else
95
95
  entry[:texture_view] = nil
96
96
  end
@@ -24,9 +24,9 @@ module WGPU
24
24
  @device = device
25
25
  desc, @descriptor_keepalive = build_descriptor(label:, entries:)
26
26
 
27
- device.push_error_scope(:validation)
28
- @handle = Native.wgpuDeviceCreateBindGroupLayout(device.handle, desc)
29
- error = device.pop_error_scope
27
+ error = device.send(:capture_error_scope) do
28
+ @handle = Native.wgpuDeviceCreateBindGroupLayout(NativeResource.checked_handle(device, expected_class: Device), desc)
29
+ end
30
30
  @descriptor_keepalive = nil
31
31
 
32
32
  if @handle.null? || (error[:type] && error[:type] != :no_error)
@@ -13,9 +13,9 @@ module WGPU
13
13
  @device = device
14
14
  desc, @pointers = build_descriptor(label:, layout:, compute:)
15
15
 
16
- device.push_error_scope(:validation)
17
- @handle = Native.wgpuDeviceCreateComputePipeline(device.handle, desc)
18
- error = device.pop_error_scope
16
+ error = device.send(:capture_error_scope) do
17
+ @handle = Native.wgpuDeviceCreateComputePipeline(NativeResource.checked_handle(device, expected_class: Device), desc)
18
+ end
19
19
 
20
20
  if @handle.null? || (error[:type] && error[:type] != :no_error)
21
21
  msg = error[:message] || "Failed to create compute pipeline"
@@ -59,7 +59,7 @@ module WGPU
59
59
  DescriptorHelpers.set_label(desc, label, keepalive: @pointers)
60
60
  desc[:layout] = normalize_layout(layout)
61
61
  desc[:compute][:next_in_chain] = nil
62
- desc[:compute][:module] = compute.fetch(:module).handle
62
+ desc[:compute][:module] = NativeResource.checked_handle(compute.fetch(:module), expected_class: ShaderModule)
63
63
  DescriptorHelpers.set_nullable_string_view(
64
64
  desc[:compute][:entry_point],
65
65
  compute[:entry_point],
@@ -72,7 +72,7 @@ module WGPU
72
72
 
73
73
  def normalize_layout(layout)
74
74
  return nil if layout.nil? || layout == :auto || layout == "auto"
75
- layout.handle
75
+ NativeResource.checked_handle(layout, expected_class: PipelineLayout)
76
76
  end
77
77
  end
78
78
  end
@@ -13,7 +13,7 @@ module WGPU
13
13
 
14
14
  layouts = Array(bind_group_layouts)
15
15
  layouts_ptr = FFI::MemoryPointer.new(:pointer, layouts.size)
16
- layouts_ptr.write_array_of_pointer(layouts.map(&:handle))
16
+ layouts_ptr.write_array_of_pointer(layouts.map { |layout| NativeResource.checked_handle(layout, expected_class: BindGroupLayout) })
17
17
 
18
18
  desc = Native::PipelineLayoutDescriptor.new
19
19
  desc[:next_in_chain] = nil
@@ -28,9 +28,9 @@ module WGPU
28
28
  desc[:bind_group_layout_count] = layouts.size
29
29
  desc[:bind_group_layouts] = layouts_ptr
30
30
 
31
- device.push_error_scope(:validation)
32
- @handle = Native.wgpuDeviceCreatePipelineLayout(device.handle, desc)
33
- error = device.pop_error_scope
31
+ error = device.send(:capture_error_scope) do
32
+ @handle = Native.wgpuDeviceCreatePipelineLayout(NativeResource.checked_handle(device, expected_class: Device), desc)
33
+ end
34
34
 
35
35
  if @handle.null? || (error[:type] && error[:type] != :no_error)
36
36
  msg = error[:message] || "Failed to create pipeline layout"
@@ -22,9 +22,9 @@ module WGPU
22
22
  fragment:
23
23
  )
24
24
 
25
- device.push_error_scope(:validation)
26
- @handle = Native.wgpuDeviceCreateRenderPipeline(device.handle, desc)
27
- error = device.pop_error_scope
25
+ error = device.send(:capture_error_scope) do
26
+ @handle = Native.wgpuDeviceCreateRenderPipeline(NativeResource.checked_handle(device, expected_class: Device), desc)
27
+ end
28
28
 
29
29
  if @handle.null? || (error[:type] && error[:type] != :no_error)
30
30
  msg = error[:message] || "Failed to create render pipeline"
@@ -78,7 +78,7 @@ module WGPU
78
78
  context: "render pipeline vertex descriptor"
79
79
  )
80
80
  vertex_state[:next_in_chain] = nil
81
- vertex_state[:module] = vertex[:module].handle
81
+ vertex_state[:module] = NativeResource.checked_handle(vertex[:module], expected_class: ShaderModule)
82
82
 
83
83
  DescriptorHelpers.set_nullable_string_view(
84
84
  vertex_state[:entry_point],
@@ -265,7 +265,7 @@ module WGPU
265
265
  frag = Native::FragmentState.new
266
266
  @pointers << frag
267
267
  frag[:next_in_chain] = nil
268
- frag[:module] = fragment[:module].handle
268
+ frag[:module] = NativeResource.checked_handle(fragment[:module], expected_class: ShaderModule)
269
269
 
270
270
  DescriptorHelpers.set_nullable_string_view(
271
271
  frag[:entry_point],
@@ -369,7 +369,7 @@ module WGPU
369
369
 
370
370
  def normalize_layout(layout)
371
371
  return nil if layout.nil? || layout == :auto || layout == "auto"
372
- layout.handle
372
+ NativeResource.checked_handle(layout, expected_class: PipelineLayout)
373
373
  end
374
374
  end
375
375
  end
@@ -30,9 +30,9 @@ module WGPU
30
30
  desc[:label][:length] = 0
31
31
  end
32
32
 
33
- device.push_error_scope(:validation)
34
- @handle = Native.wgpuDeviceCreateShaderModule(device.handle, desc)
35
- error = device.pop_error_scope
33
+ error = device.send(:capture_error_scope) do
34
+ @handle = Native.wgpuDeviceCreateShaderModule(NativeResource.checked_handle(device, expected_class: Device), desc)
35
+ end
36
36
 
37
37
  if @handle.null? || (error[:type] && error[:type] != :no_error)
38
38
  msg = error[:message] || "Failed to create shader module"