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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c118a2950771b2f34f06fcf4642634e57e8b1de07c26681b81aa94d6a1faaf93
4
- data.tar.gz: b4f4cc4e97846d21e74144194f97ede7cb2704725cb5887a885345ceedd1c8f1
3
+ metadata.gz: b1ef9011b4b35861933cac88658c5f5a8c3dba6c3570a22f54109b0bb2b5f0c8
4
+ data.tar.gz: 33fef1fd859ad532a49e27d154f6e5fc303bf9ac29bd698a955e14a5b18c58ea
5
5
  SHA512:
6
- metadata.gz: 5fad367a0475a31c846fa57151a01d6fda7aed9a5592f6167237117da8967df7269a7ac0233059a174be0803794dc844ee4660cd1383ce3fa858009620100036
7
- data.tar.gz: 967d67b899a1ca01e5e805407677772aeaec152d72ede7b31746a8baef6d5fa04544863f7d8e70a4bb2804506ccf8fdd58036c5d5a99c49cfe338be73d667504
6
+ metadata.gz: f11393f0e2cb7cfa5b6f4e1eb2062f221654e45622c2e5f6ac45d9ffcd92e2e08dc101f81f73eb576cc21d7c42b832f0d418f3f5ccd3a502a8f66f24271d6117
7
+ data.tar.gz: a122894e157f437c429675f80819cc69f1198e54f6ba8188e7eaedb5ba32c431738893fd28ff911bd8f6917ef4fdb73eff4bc8541346150431c095494b4a11a2
data/CHANGELOG.md CHANGED
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 1.2.2 - 2026-09-09
11
+
12
+ ### Fixed
13
+
14
+ - Invalidate mapped buffer views after unmap, destroy, or release, and check
15
+ accesses against the actual mapped interval, including after remapping.
16
+ - Reject copies of native resource wrappers and validate resource argument
17
+ types and release state before passing handles to native code.
18
+ - Validate buffer clear ranges and skip zero-length clears that would panic
19
+ in the pinned native library.
20
+ - Reject empty error-scope pops, serialize nested scopes per device, start
21
+ asynchronous pops on the calling thread, and clean up scopes when blocks
22
+ raise without replacing the original exception.
23
+ - Release native handles after failed resource initialization and free native
24
+ feature arrays after converting them to Ruby values.
25
+ - Validate timeouts before starting native operations and reclaim adapter
26
+ and device handles when waiting fails.
27
+ - Accept Array texture extents consistently across creation, copies, writes,
28
+ and readback through shared descriptor normalization.
29
+ - Distinguish raw Wayland surface pointers from existing WGPU surfaces in
30
+ `CanvasContext`, with explicit `wl_surface:` and `wgpu_surface:` keys.
31
+
32
+ ### Changed
33
+
34
+ - Allow rubyzip 3.x alongside 2.x.
35
+
10
36
  ## 1.2.1 - 2026-07-24
11
37
 
12
38
  ### Added
data/docs/async.md CHANGED
@@ -27,7 +27,9 @@ Thread implementation until those native entry points are implemented.
27
27
  Synchronous adapter/device requests, `Buffer#map_sync`,
28
28
  `Device#pop_error_scope`, and `Queue#on_submitted_work_done` accept
29
29
  `timeout:` in seconds. The default `nil` preserves the prior unbounded wait.
30
- Expiry raises `WGPU::TimeoutError`.
30
+ Expiry raises `WGPU::TimeoutError`. Timeouts must be finite and non-negative;
31
+ invalid values are rejected before starting the native operation. Adapter and
32
+ device requests also reclaim their handles if waiting fails for another reason.
31
33
 
32
34
  `Device.request` can time out only when its adapter is associated with an
33
35
  `Instance`. When adopting a native adapter, pass `instance:` to
data/docs/errors.md CHANGED
@@ -8,6 +8,19 @@
8
8
  maps WebGPU error types to `ValidationError`, `OutOfMemoryError`,
9
9
  `InternalError`, or `DeviceLostError`.
10
10
 
11
+ `with_error_scope { ... }` always pops its scope, including when the block
12
+ raises; the original block exception is preserved. Popping without a scope
13
+ owned by the current thread raises `WGPU::DeviceError` before calling native
14
+ code.
15
+
16
+ Scopes are nested and serialized per device from push through native pop.
17
+ Push and pop on the same thread, and finish the scope before waiting for
18
+ another thread's scoped work on that device. In particular, do not wait for
19
+ an asynchronous pipeline creation inside `with_error_scope`.
20
+ `pop_error_scope_async` starts the pop immediately on the calling thread and
21
+ waits for its result in the background, so a later push cannot change which
22
+ scope it pops.
23
+
11
24
  Device-level callbacks can be installed after device creation:
12
25
 
13
26
  ```ruby
@@ -57,6 +57,11 @@ their window toolkit to `Surface.from_metal_layer`,
57
57
  native window/display/layer alive longer than the surface; wgpu-ruby does not
58
58
  take ownership of those platform objects.
59
59
 
60
+ For `CanvasContext`, pass an existing surface as `wgpu_surface: surface`.
61
+ Wayland platform descriptors use `platform: :wayland`, `display: wl_display`,
62
+ and `wl_surface: wl_surface_pointer`. The older `surface:` key remains
63
+ supported for existing WGPU surfaces and raw Wayland pointers.
64
+
60
65
  [`examples/09_clear_color.rb`](../examples/09_clear_color.rb) is the smallest
61
66
  resizable render loop. The other rendering examples cover vertex/index
62
67
  buffers, textures, bind groups, depth testing, and uniform updates.
@@ -33,6 +33,15 @@ bytes = device.create_buffer(
33
33
  end
34
34
  ```
35
35
 
36
+ Native wrappers reject `dup` and `clone` with `TypeError`. Resource arguments
37
+ are checked for the expected wrapper type and release state before their
38
+ handles enter FFI. Failed constructors release any native handle they acquired.
39
+
40
+ Mapped views retain their buffer, but become invalid after `unmap`, `destroy`,
41
+ or `release`. Reading or writing an invalid view raises `WGPU::BufferError`,
42
+ even after the buffer is mapped again. Accesses must fit within the current
43
+ mapped interval, not just within the buffer's total size.
44
+
36
45
  ## Wrapper matrix
37
46
 
38
47
  The table covers all 23 native wrapper classes. `BufferMappedRange`,
@@ -24,7 +24,7 @@ module WGPU
24
24
  desc[:label][:length] = 0
25
25
  end
26
26
 
27
- @handle = Native.wgpuDeviceCreateCommandEncoder(device.handle, desc)
27
+ @handle = Native.wgpuDeviceCreateCommandEncoder(NativeResource.checked_handle(device, expected_class: Device), desc)
28
28
  raise CommandError, "Failed to create command encoder" if @handle.null?
29
29
  end
30
30
 
@@ -86,8 +86,8 @@ module WGPU
86
86
  raise CommandError, "Encoder already finished" if @finished
87
87
  Native.wgpuCommandEncoderCopyBufferToBuffer(
88
88
  @handle,
89
- source.handle, source_offset,
90
- destination.handle, destination_offset,
89
+ NativeResource.checked_handle(source, expected_class: Buffer), source_offset,
90
+ NativeResource.checked_handle(destination, expected_class: Buffer), destination_offset,
91
91
  size
92
92
  )
93
93
  end
@@ -100,19 +100,16 @@ module WGPU
100
100
  def copy_buffer_to_texture(source:, destination:, copy_size:)
101
101
  raise CommandError, "Encoder already finished" if @finished
102
102
 
103
- size = Native::Extent3D.new
104
- size[:width] = copy_size[:width] || copy_size[0]
105
- size[:height] = copy_size[:height] || copy_size[1] || 1
106
- size[:depth_or_array_layers] = copy_size[:depth_or_array_layers] || copy_size[2] || 1
103
+ size = DescriptorHelpers.extent_3d(copy_size)
107
104
 
108
105
  src = Native::ImageCopyBuffer.new
109
106
  src[:layout][:offset] = source[:offset] || 0
110
107
  src[:layout][:bytes_per_row] = source[:bytes_per_row]
111
108
  src[:layout][:rows_per_image] = source[:rows_per_image] || size[:height]
112
- src[:buffer] = source[:buffer].handle
109
+ src[:buffer] = NativeResource.checked_handle(source[:buffer], expected_class: Buffer)
113
110
 
114
111
  dst = Native::ImageCopyTexture.new
115
- dst[:texture] = destination[:texture].handle
112
+ dst[:texture] = NativeResource.checked_handle(destination[:texture], expected_class: Texture)
116
113
  dst[:mip_level] = destination[:mip_level] || 0
117
114
  dst[:origin][:x] = destination.dig(:origin, :x) || 0
118
115
  dst[:origin][:y] = destination.dig(:origin, :y) || 0
@@ -134,13 +131,10 @@ module WGPU
134
131
  def copy_texture_to_buffer(source:, destination:, copy_size:)
135
132
  raise CommandError, "Encoder already finished" if @finished
136
133
 
137
- size = Native::Extent3D.new
138
- size[:width] = copy_size[:width] || copy_size[0]
139
- size[:height] = copy_size[:height] || copy_size[1] || 1
140
- size[:depth_or_array_layers] = copy_size[:depth_or_array_layers] || copy_size[2] || 1
134
+ size = DescriptorHelpers.extent_3d(copy_size)
141
135
 
142
136
  src = Native::ImageCopyTexture.new
143
- src[:texture] = source[:texture].handle
137
+ src[:texture] = NativeResource.checked_handle(source[:texture], expected_class: Texture)
144
138
  src[:mip_level] = source[:mip_level] || 0
145
139
  src[:origin][:x] = source.dig(:origin, :x) || 0
146
140
  src[:origin][:y] = source.dig(:origin, :y) || 0
@@ -155,7 +149,7 @@ module WGPU
155
149
  dst[:layout][:offset] = destination[:offset] || 0
156
150
  dst[:layout][:bytes_per_row] = destination[:bytes_per_row]
157
151
  dst[:layout][:rows_per_image] = destination[:rows_per_image] || size[:height]
158
- dst[:buffer] = destination[:buffer].handle
152
+ dst[:buffer] = NativeResource.checked_handle(destination[:buffer], expected_class: Buffer)
159
153
 
160
154
  Native.wgpuCommandEncoderCopyTextureToBuffer(@handle, src, dst, size)
161
155
  end
@@ -169,7 +163,7 @@ module WGPU
169
163
  raise CommandError, "Encoder already finished" if @finished
170
164
 
171
165
  src = Native::ImageCopyTexture.new
172
- src[:texture] = source[:texture].handle
166
+ src[:texture] = NativeResource.checked_handle(source[:texture], expected_class: Texture)
173
167
  src[:mip_level] = source[:mip_level] || 0
174
168
  src[:origin][:x] = source.dig(:origin, :x) || 0
175
169
  src[:origin][:y] = source.dig(:origin, :y) || 0
@@ -181,7 +175,7 @@ module WGPU
181
175
  )
182
176
 
183
177
  dst = Native::ImageCopyTexture.new
184
- dst[:texture] = destination[:texture].handle
178
+ dst[:texture] = NativeResource.checked_handle(destination[:texture], expected_class: Texture)
185
179
  dst[:mip_level] = destination[:mip_level] || 0
186
180
  dst[:origin][:x] = destination.dig(:origin, :x) || 0
187
181
  dst[:origin][:y] = destination.dig(:origin, :y) || 0
@@ -192,10 +186,7 @@ module WGPU
192
186
  name: "destination texture aspect"
193
187
  )
194
188
 
195
- size = Native::Extent3D.new
196
- size[:width] = copy_size[:width] || copy_size[0]
197
- size[:height] = copy_size[:height] || copy_size[1] || 1
198
- size[:depth_or_array_layers] = copy_size[:depth_or_array_layers] || copy_size[2] || 1
189
+ size = DescriptorHelpers.extent_3d(copy_size)
199
190
 
200
191
  Native.wgpuCommandEncoderCopyTextureToTexture(@handle, src, dst, size)
201
192
  end
@@ -206,10 +197,10 @@ module WGPU
206
197
  raise CommandError, "Encoder already finished" if @finished
207
198
  Native.wgpuCommandEncoderResolveQuerySet(
208
199
  @handle,
209
- query_set.handle,
200
+ NativeResource.checked_handle(query_set, expected_class: QuerySet),
210
201
  first_query,
211
202
  query_count,
212
- destination.handle,
203
+ NativeResource.checked_handle(destination, expected_class: Buffer),
213
204
  destination_offset
214
205
  )
215
206
  end
@@ -221,8 +212,15 @@ module WGPU
221
212
  # @return [void]
222
213
  def clear_buffer(buffer, offset: 0, size: nil)
223
214
  raise CommandError, "Encoder already finished" if @finished
224
- size ||= buffer.size - offset
225
- Native.wgpuCommandEncoderClearBuffer(@handle, buffer.handle, offset, size)
215
+ buffer_handle = NativeResource.checked_handle(buffer, expected_class: Buffer)
216
+ offset = DataTypes.validate_alignment!(offset, 4, name: "clear offset")
217
+ size = DataTypes.validate_alignment!(size || (buffer.size - offset), 4, name: "clear size")
218
+ if offset > buffer.size || size > buffer.size - offset
219
+ raise ArgumentError, "clear range exceeds buffer size"
220
+ end
221
+ return if size.zero?
222
+
223
+ Native.wgpuCommandEncoderClearBuffer(@handle, buffer_handle, offset, size)
226
224
  end
227
225
 
228
226
  # Writes a GPU timestamp to a query set.
@@ -231,7 +229,7 @@ module WGPU
231
229
  # @return [void]
232
230
  def write_timestamp(query_set, query_index)
233
231
  raise CommandError, "Encoder already finished" if @finished
234
- Native.wgpuCommandEncoderWriteTimestamp(@handle, query_set.handle, query_index)
232
+ Native.wgpuCommandEncoderWriteTimestamp(@handle, NativeResource.checked_handle(query_set, expected_class: QuerySet), query_index)
235
233
  end
236
234
 
237
235
  # Starts a labeled group in GPU debugging tools.
@@ -25,7 +25,7 @@ module WGPU
25
25
  @timestamp_writes = nil
26
26
  if timestamp_writes
27
27
  @timestamp_writes = Native::ComputePassTimestampWrites.new
28
- @timestamp_writes[:query_set] = timestamp_writes.fetch(:query_set).handle
28
+ @timestamp_writes[:query_set] = NativeResource.checked_handle(timestamp_writes.fetch(:query_set), expected_class: QuerySet)
29
29
  @timestamp_writes[:beginning_of_pass_write_index] = timestamp_writes[:beginning_of_pass_write_index] || 0xFFFFFFFF
30
30
  @timestamp_writes[:end_of_pass_write_index] = timestamp_writes[:end_of_pass_write_index] || 0xFFFFFFFF
31
31
  desc[:timestamp_writes] = @timestamp_writes.to_ptr
@@ -33,7 +33,7 @@ module WGPU
33
33
  desc[:timestamp_writes] = nil
34
34
  end
35
35
 
36
- @handle = Native.wgpuCommandEncoderBeginComputePass(encoder.handle, desc)
36
+ @handle = Native.wgpuCommandEncoderBeginComputePass(NativeResource.checked_handle(encoder, expected_class: CommandEncoder), desc)
37
37
  raise CommandError, "Failed to begin compute pass" if @handle.null?
38
38
  end
39
39
 
@@ -41,7 +41,7 @@ module WGPU
41
41
  # @param pipeline [ComputePipeline] pipeline to bind
42
42
  # @return [void]
43
43
  def set_pipeline(pipeline)
44
- Native.wgpuComputePassEncoderSetPipeline(@handle, pipeline.handle)
44
+ Native.wgpuComputePassEncoderSetPipeline(@handle, NativeResource.checked_handle(pipeline, expected_class: ComputePipeline))
45
45
  end
46
46
 
47
47
  # Binds a resource group for subsequent dispatches.
@@ -51,11 +51,11 @@ module WGPU
51
51
  # @return [void]
52
52
  def set_bind_group(index, bind_group, dynamic_offsets: [])
53
53
  if dynamic_offsets.empty?
54
- Native.wgpuComputePassEncoderSetBindGroup(@handle, index, bind_group.handle, 0, nil)
54
+ Native.wgpuComputePassEncoderSetBindGroup(@handle, index, NativeResource.checked_handle(bind_group, expected_class: BindGroup), 0, nil)
55
55
  else
56
56
  offsets_ptr = FFI::MemoryPointer.new(:uint32, dynamic_offsets.size)
57
57
  offsets_ptr.write_array_of_uint32(dynamic_offsets)
58
- Native.wgpuComputePassEncoderSetBindGroup(@handle, index, bind_group.handle, dynamic_offsets.size, offsets_ptr)
58
+ Native.wgpuComputePassEncoderSetBindGroup(@handle, index, NativeResource.checked_handle(bind_group, expected_class: BindGroup), dynamic_offsets.size, offsets_ptr)
59
59
  end
60
60
  end
61
61
 
@@ -73,7 +73,7 @@ module WGPU
73
73
  # @param offset [Integer] byte offset of the arguments
74
74
  # @return [void]
75
75
  def dispatch_workgroups_indirect(buffer, offset: 0)
76
- Native.wgpuComputePassEncoderDispatchWorkgroupsIndirect(@handle, buffer.handle, offset)
76
+ Native.wgpuComputePassEncoderDispatchWorkgroupsIndirect(@handle, NativeResource.checked_handle(buffer, expected_class: Buffer), offset)
77
77
  end
78
78
 
79
79
  # Starts a labeled group in GPU debugging tools.
@@ -47,7 +47,7 @@ module WGPU
47
47
  desc[:depth_read_only] = depth_read_only ? 1 : 0
48
48
  desc[:stencil_read_only] = stencil_read_only ? 1 : 0
49
49
 
50
- @handle = Native.wgpuDeviceCreateRenderBundleEncoder(device.handle, desc)
50
+ @handle = Native.wgpuDeviceCreateRenderBundleEncoder(NativeResource.checked_handle(device, expected_class: Device), desc)
51
51
  raise RenderBundleError, "Failed to create render bundle encoder" if @handle.null?
52
52
  end
53
53
 
@@ -58,7 +58,7 @@ module WGPU
58
58
  def set_pipeline(pipeline)
59
59
  raise RenderBundleError, "Encoder already finished" if @finished
60
60
 
61
- Native.wgpuRenderBundleEncoderSetPipeline(@handle, pipeline.handle)
61
+ Native.wgpuRenderBundleEncoderSetPipeline(@handle, NativeResource.checked_handle(pipeline, expected_class: RenderPipeline))
62
62
  end
63
63
 
64
64
  # Binds a resource group for subsequent bundle draws.
@@ -72,9 +72,9 @@ module WGPU
72
72
  if dynamic_offsets && !dynamic_offsets.empty?
73
73
  offsets_ptr = FFI::MemoryPointer.new(:uint32, dynamic_offsets.size)
74
74
  offsets_ptr.write_array_of_uint32(dynamic_offsets)
75
- Native.wgpuRenderBundleEncoderSetBindGroup(@handle, index, bind_group.handle, dynamic_offsets.size, offsets_ptr)
75
+ Native.wgpuRenderBundleEncoderSetBindGroup(@handle, index, NativeResource.checked_handle(bind_group, expected_class: BindGroup), dynamic_offsets.size, offsets_ptr)
76
76
  else
77
- Native.wgpuRenderBundleEncoderSetBindGroup(@handle, index, bind_group.handle, 0, nil)
77
+ Native.wgpuRenderBundleEncoderSetBindGroup(@handle, index, NativeResource.checked_handle(bind_group, expected_class: BindGroup), 0, nil)
78
78
  end
79
79
  end
80
80
 
@@ -86,7 +86,7 @@ module WGPU
86
86
  raise RenderBundleError, "Encoder already finished" if @finished
87
87
 
88
88
  size ||= buffer.size - offset
89
- Native.wgpuRenderBundleEncoderSetVertexBuffer(@handle, slot, buffer.handle, offset, size)
89
+ Native.wgpuRenderBundleEncoderSetVertexBuffer(@handle, slot, NativeResource.checked_handle(buffer, expected_class: Buffer), offset, size)
90
90
  end
91
91
 
92
92
  # Binds an index buffer for indexed bundle draws.
@@ -98,7 +98,7 @@ module WGPU
98
98
 
99
99
  size ||= buffer.size - offset
100
100
  format_value = Native::EnumHelper.coerce(Native::IndexFormat, format, name: "index format")
101
- Native.wgpuRenderBundleEncoderSetIndexBuffer(@handle, buffer.handle, format_value, offset, size)
101
+ Native.wgpuRenderBundleEncoderSetIndexBuffer(@handle, NativeResource.checked_handle(buffer, expected_class: Buffer), format_value, offset, size)
102
102
  end
103
103
 
104
104
  # Records a non-indexed draw in the bundle.
@@ -124,7 +124,7 @@ module WGPU
124
124
  def draw_indirect(buffer, offset: 0)
125
125
  raise RenderBundleError, "Encoder already finished" if @finished
126
126
 
127
- Native.wgpuRenderBundleEncoderDrawIndirect(@handle, buffer.handle, offset)
127
+ Native.wgpuRenderBundleEncoderDrawIndirect(@handle, NativeResource.checked_handle(buffer, expected_class: Buffer), offset)
128
128
  end
129
129
 
130
130
  # Records an indexed draw using buffer arguments.
@@ -134,7 +134,7 @@ module WGPU
134
134
  def draw_indexed_indirect(buffer, offset: 0)
135
135
  raise RenderBundleError, "Encoder already finished" if @finished
136
136
 
137
- Native.wgpuRenderBundleEncoderDrawIndexedIndirect(@handle, buffer.handle, offset)
137
+ Native.wgpuRenderBundleEncoderDrawIndexedIndirect(@handle, NativeResource.checked_handle(buffer, expected_class: Buffer), offset)
138
138
  end
139
139
 
140
140
  # Starts a labeled group in GPU debugging tools.
@@ -34,10 +34,10 @@ module WGPU
34
34
  desc[:depth_stencil_attachment] = nil
35
35
  end
36
36
 
37
- desc[:occlusion_query_set] = occlusion_query_set&.handle
37
+ desc[:occlusion_query_set] = (occlusion_query_set && NativeResource.checked_handle(occlusion_query_set, expected_class: QuerySet))
38
38
  if timestamp_writes
39
39
  ts = Native::RenderPassTimestampWrites.new
40
- ts[:query_set] = timestamp_writes.fetch(:query_set).handle
40
+ ts[:query_set] = NativeResource.checked_handle(timestamp_writes.fetch(:query_set), expected_class: QuerySet)
41
41
  ts[:beginning_of_pass_write_index] = timestamp_writes[:beginning_of_pass_write_index] || 0xFFFFFFFF
42
42
  ts[:end_of_pass_write_index] = timestamp_writes[:end_of_pass_write_index] || 0xFFFFFFFF
43
43
  @pointers << ts
@@ -46,7 +46,7 @@ module WGPU
46
46
  desc[:timestamp_writes] = nil
47
47
  end
48
48
 
49
- @handle = Native.wgpuCommandEncoderBeginRenderPass(encoder.handle, desc)
49
+ @handle = Native.wgpuCommandEncoderBeginRenderPass(NativeResource.checked_handle(encoder, expected_class: CommandEncoder), desc)
50
50
  raise CommandError, "Failed to begin render pass" if @handle.null?
51
51
  end
52
52
 
@@ -54,7 +54,7 @@ module WGPU
54
54
  # @param pipeline [RenderPipeline] pipeline to bind
55
55
  # @return [void]
56
56
  def set_pipeline(pipeline)
57
- Native.wgpuRenderPassEncoderSetPipeline(@handle, pipeline.handle)
57
+ Native.wgpuRenderPassEncoderSetPipeline(@handle, NativeResource.checked_handle(pipeline, expected_class: RenderPipeline))
58
58
  end
59
59
 
60
60
  # Binds a resource group for subsequent draws.
@@ -64,11 +64,11 @@ module WGPU
64
64
  # @return [void]
65
65
  def set_bind_group(index, bind_group, dynamic_offsets: [])
66
66
  if dynamic_offsets.empty?
67
- Native.wgpuRenderPassEncoderSetBindGroup(@handle, index, bind_group.handle, 0, nil)
67
+ Native.wgpuRenderPassEncoderSetBindGroup(@handle, index, NativeResource.checked_handle(bind_group, expected_class: BindGroup), 0, nil)
68
68
  else
69
69
  offsets_ptr = FFI::MemoryPointer.new(:uint32, dynamic_offsets.size)
70
70
  offsets_ptr.write_array_of_uint32(dynamic_offsets)
71
- Native.wgpuRenderPassEncoderSetBindGroup(@handle, index, bind_group.handle, dynamic_offsets.size, offsets_ptr)
71
+ Native.wgpuRenderPassEncoderSetBindGroup(@handle, index, NativeResource.checked_handle(bind_group, expected_class: BindGroup), dynamic_offsets.size, offsets_ptr)
72
72
  end
73
73
  end
74
74
 
@@ -80,7 +80,7 @@ module WGPU
80
80
  # @return [void]
81
81
  def set_vertex_buffer(slot, buffer, offset: 0, size: nil)
82
82
  size ||= buffer.size - offset
83
- Native.wgpuRenderPassEncoderSetVertexBuffer(@handle, slot, buffer.handle, offset, size)
83
+ Native.wgpuRenderPassEncoderSetVertexBuffer(@handle, slot, NativeResource.checked_handle(buffer, expected_class: Buffer), offset, size)
84
84
  end
85
85
 
86
86
  # Binds an index buffer for indexed draws.
@@ -92,7 +92,7 @@ module WGPU
92
92
  def set_index_buffer(buffer, format, offset: 0, size: nil)
93
93
  size ||= buffer.size - offset
94
94
  format_value = Native::EnumHelper.coerce(Native::IndexFormat, format, name: "index format")
95
- Native.wgpuRenderPassEncoderSetIndexBuffer(@handle, buffer.handle, format_value, offset, size)
95
+ Native.wgpuRenderPassEncoderSetIndexBuffer(@handle, NativeResource.checked_handle(buffer, expected_class: Buffer), format_value, offset, size)
96
96
  end
97
97
 
98
98
  # Records a non-indexed draw.
@@ -161,7 +161,7 @@ module WGPU
161
161
  # @param offset [Integer] byte offset of the arguments
162
162
  # @return [void]
163
163
  def draw_indirect(buffer, offset: 0)
164
- Native.wgpuRenderPassEncoderDrawIndirect(@handle, buffer.handle, offset)
164
+ Native.wgpuRenderPassEncoderDrawIndirect(@handle, NativeResource.checked_handle(buffer, expected_class: Buffer), offset)
165
165
  end
166
166
 
167
167
  # Draws using indexed arguments stored in a buffer.
@@ -170,7 +170,7 @@ module WGPU
170
170
  # @param offset [Integer] byte offset of the arguments
171
171
  # @return [void]
172
172
  def draw_indexed_indirect(buffer, offset: 0)
173
- Native.wgpuRenderPassEncoderDrawIndexedIndirect(@handle, buffer.handle, offset)
173
+ Native.wgpuRenderPassEncoderDrawIndexedIndirect(@handle, NativeResource.checked_handle(buffer, expected_class: Buffer), offset)
174
174
  end
175
175
 
176
176
  # Executes pre-recorded render bundles.
@@ -178,7 +178,7 @@ module WGPU
178
178
  # @param bundles [Array<RenderBundle>] bundles to execute in order
179
179
  # @return [void]
180
180
  def execute_bundles(bundles)
181
- bundle_handles = bundles.map(&:handle)
181
+ bundle_handles = bundles.map { |bundle| NativeResource.checked_handle(bundle, expected_class: RenderBundle) }
182
182
  bundles_ptr = FFI::MemoryPointer.new(:pointer, bundle_handles.size)
183
183
  bundles_ptr.write_array_of_pointer(bundle_handles)
184
184
  Native.wgpuRenderPassEncoderExecuteBundles(@handle, bundle_handles.size, bundles_ptr)
@@ -283,9 +283,9 @@ module WGPU
283
283
  attachments.each_with_index do |att, i|
284
284
  ca = Native::RenderPassColorAttachment.new(ptr + (i * Native::RenderPassColorAttachment.size))
285
285
  ca[:next_in_chain] = nil
286
- ca[:view] = att[:view].handle
286
+ ca[:view] = NativeResource.checked_handle(att[:view], expected_class: TextureView)
287
287
  ca[:depth_slice] = att[:depth_slice] || 0xFFFFFFFF
288
- ca[:resolve_target] = att[:resolve_target]&.handle
288
+ ca[:resolve_target] = (att[:resolve_target] && NativeResource.checked_handle(att[:resolve_target], expected_class: TextureView))
289
289
  ca[:load_op] = Native::EnumHelper.coerce(Native::LoadOp, att[:load_op] || :clear, name: "load op")
290
290
  ca[:store_op] = Native::EnumHelper.coerce(Native::StoreOp, att[:store_op] || :store, name: "store op")
291
291
 
@@ -303,7 +303,7 @@ module WGPU
303
303
  ds = Native::RenderPassDepthStencilAttachment.new
304
304
  @pointers << ds
305
305
 
306
- ds[:view] = att[:view].handle
306
+ ds[:view] = NativeResource.checked_handle(att[:view], expected_class: TextureView)
307
307
  ds[:depth_load_op] = Native::EnumHelper.coerce(
308
308
  Native::LoadOp,
309
309
  att[:depth_load_op] || :clear,
@@ -28,6 +28,7 @@ module WGPU
28
28
  # @raise [TimeoutError] if the request exceeds +timeout+
29
29
  def self.request(instance, power_preference: :high_performance, backend: nil, feature_level: :core,
30
30
  force_fallback_adapter: false, compatible_surface: nil, timeout: nil)
31
+ timeout = AsyncWaiter.normalize_timeout(timeout)
31
32
  adapter_ptr = FFI::MemoryPointer.new(:pointer)
32
33
  status_holder = {
33
34
  done: false,
@@ -80,7 +81,7 @@ module WGPU
80
81
  backend || :undefined,
81
82
  name: "backend type"
82
83
  )
83
- options[:compatible_surface] = compatible_surface&.handle
84
+ options[:compatible_surface] = (compatible_surface && NativeResource.checked_handle(compatible_surface, expected_class: Surface))
84
85
 
85
86
  callback_info = Native::RequestAdapterCallbackInfo.new
86
87
  callback_info[:next_in_chain] = nil
@@ -92,7 +93,7 @@ module WGPU
92
93
  callback_token = CallbackKeepalive.retain(instance, callback)
93
94
  future =
94
95
  begin
95
- Native.wgpuInstanceRequestAdapter(instance.handle, options, callback_info)
96
+ Native.wgpuInstanceRequestAdapter(NativeResource.checked_handle(instance, expected_class: Instance), options, callback_info)
96
97
  rescue StandardError
97
98
  CallbackKeepalive.release(instance, callback_token)
98
99
  raise
@@ -100,7 +101,7 @@ module WGPU
100
101
 
101
102
  begin
102
103
  AsyncWaiter.wait(status_holder: status_holder, instance: instance, future: future, timeout: timeout)
103
- rescue TimeoutError
104
+ rescue StandardError
104
105
  abandoned_adapter = status_holder[:mutex].synchronize do
105
106
  status_holder[:abandoned] = true
106
107
  next unless status_holder[:done] && !status_holder[:cleanup_claimed]
@@ -212,6 +213,8 @@ module WGPU
212
213
  end
213
214
  end
214
215
  result
216
+ ensure
217
+ Native.wgpuSupportedFeaturesFreeMembers(supported) if supported
215
218
  end
216
219
 
217
220
  # Reports whether the adapter supports a feature.
@@ -45,8 +45,7 @@ module WGPU
45
45
  # @return [void]
46
46
  # @raise [TimeoutError] if completion exceeds the timeout
47
47
  def wait(status_holder:, instance: nil, device: nil, future: nil, timeout: nil)
48
- timeout = Float(timeout) if timeout
49
- raise ArgumentError, "timeout must be non-negative" if timeout&.negative?
48
+ timeout = normalize_timeout(timeout)
50
49
 
51
50
  deadline = monotonic_time + timeout if timeout
52
51
  wait_info = build_wait_info(future) if instance && Native.future_api?
@@ -60,12 +59,24 @@ module WGPU
60
59
  elsif instance
61
60
  instance.process_events
62
61
  elsif device && Native.device_poll_available?
63
- Native.wgpuDevicePoll(device.handle, 0, nil)
62
+ Native.wgpuDevicePoll(NativeResource.checked_handle(device, expected_class: Device), 0, nil)
64
63
  end
65
64
  sleep(poll_interval) unless status_holder[:done] || waited
66
65
  end
67
66
  end
68
67
 
68
+ # Validates a timeout before any native operation is started.
69
+ def normalize_timeout(timeout)
70
+ return nil if timeout.nil?
71
+
72
+ value = Float(timeout)
73
+ raise ArgumentError, "timeout must be finite and non-negative" unless value.finite? && value >= 0
74
+
75
+ value
76
+ rescue TypeError
77
+ raise ArgumentError, "timeout must be a number"
78
+ end
79
+
69
80
  def callback_mode_value(name)
70
81
  Native::CallbackMode[name] || CALLBACK_MODE_FALLBACK.fetch(name)
71
82
  end
@@ -87,7 +98,7 @@ module WGPU
87
98
  private_class_method :build_wait_info
88
99
 
89
100
  def wait_with_wait_any(instance, wait_info)
90
- status = Native.wgpuInstanceWaitAny(instance.handle, 1, wait_info.to_ptr, 0)
101
+ status = Native.wgpuInstanceWaitAny(NativeResource.checked_handle(instance, expected_class: Instance), 1, wait_info.to_ptr, 0)
91
102
  return false if [:success, :timed_out].include?(status)
92
103
 
93
104
  raise Error, "wgpuInstanceWaitAny failed: #{status.inspect}"
@@ -10,7 +10,10 @@ module WGPU
10
10
  def initialize(instance, present_info = {})
11
11
  @instance = instance
12
12
  @present_info = present_info || {}
13
- @surface = @present_info[:surface]
13
+ @surface = @present_info[:wgpu_surface]
14
+ if @present_info[:surface].is_a?(Surface) || ![:wayland, :linux_wayland].include?(@present_info[:platform]&.to_sym)
15
+ @surface ||= @present_info[:surface]
16
+ end
14
17
  @physical_size = [0, 0]
15
18
  @config = nil
16
19
  end
@@ -125,7 +128,7 @@ module WGPU
125
128
  when :x11, :linux_x11
126
129
  Surface.from_xlib_window(@instance, @present_info.fetch(:display), @present_info.fetch(:window))
127
130
  when :wayland, :linux_wayland
128
- Surface.from_wayland_surface(@instance, @present_info.fetch(:display), @present_info.fetch(:surface))
131
+ Surface.from_wayland_surface(@instance, @present_info.fetch(:display), @present_info.fetch(:wl_surface) { @present_info.fetch(:surface) })
129
132
  else
130
133
  raise SurfaceError, "Cannot build surface from present_info: #{@present_info.inspect}"
131
134
  end