wgpu 1.2.0 → 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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +66 -0
  3. data/README.md +18 -2
  4. data/docs/README.md +2 -2
  5. data/docs/api_coverage.md +23 -15
  6. data/docs/async.md +13 -1
  7. data/docs/command_encoding.md +25 -0
  8. data/docs/errors.md +13 -0
  9. data/docs/getting_started_compute.md +1 -0
  10. data/docs/getting_started_rendering.md +5 -0
  11. data/docs/pipeline_descriptors.md +7 -2
  12. data/docs/releasing.md +11 -1
  13. data/docs/resource_lifetime.md +25 -2
  14. data/docs/texture_readback.md +27 -0
  15. data/docs/troubleshooting.md +5 -0
  16. data/docs/upgrading_wgpu_native.md +16 -5
  17. data/lib/wgpu/async_task.rb +19 -0
  18. data/lib/wgpu/commands/command_buffer.rb +14 -1
  19. data/lib/wgpu/commands/command_encoder.rb +82 -27
  20. data/lib/wgpu/commands/compute_pass.rb +49 -6
  21. data/lib/wgpu/commands/render_bundle.rb +9 -1
  22. data/lib/wgpu/commands/render_bundle_encoder.rb +63 -9
  23. data/lib/wgpu/commands/render_pass.rb +116 -14
  24. data/lib/wgpu/core/adapter.rb +95 -13
  25. data/lib/wgpu/core/async_waiter.rb +30 -4
  26. data/lib/wgpu/core/canvas_context.rb +37 -2
  27. data/lib/wgpu/core/device.rb +377 -70
  28. data/lib/wgpu/core/instance.rb +20 -0
  29. data/lib/wgpu/core/queue.rb +143 -43
  30. data/lib/wgpu/core/surface.rb +55 -7
  31. data/lib/wgpu/data_types.rb +16 -0
  32. data/lib/wgpu/descriptor_helpers.rb +87 -0
  33. data/lib/wgpu/error.rb +15 -0
  34. data/lib/wgpu/native/abi_verifier.rb +37 -3
  35. data/lib/wgpu/native/callbacks.rb +6 -0
  36. data/lib/wgpu/native/capabilities.rb +16 -2
  37. data/lib/wgpu/native/distribution.rb +63 -0
  38. data/lib/wgpu/native/enum_helper.rb +17 -0
  39. data/lib/wgpu/native/enums.rb +8 -0
  40. data/lib/wgpu/native/fixtures/webgpu-v27.0.4.0-enums.h +848 -0
  41. data/lib/wgpu/native/functions.rb +14 -0
  42. data/lib/wgpu/native/installer.rb +55 -18
  43. data/lib/wgpu/native/loader.rb +22 -0
  44. data/lib/wgpu/native/structs.rb +18 -1
  45. data/lib/wgpu/native_resource.rb +190 -3
  46. data/lib/wgpu/pipeline/bind_group.rb +16 -7
  47. data/lib/wgpu/pipeline/bind_group_layout.rb +20 -7
  48. data/lib/wgpu/pipeline/compute_pipeline.rb +25 -37
  49. data/lib/wgpu/pipeline/pipeline_layout.rb +12 -4
  50. data/lib/wgpu/pipeline/render_pipeline.rb +33 -48
  51. data/lib/wgpu/pipeline/shader_module.rb +57 -32
  52. data/lib/wgpu/resources/buffer.rb +330 -59
  53. data/lib/wgpu/resources/query_set.rb +13 -1
  54. data/lib/wgpu/resources/sampler.rb +10 -3
  55. data/lib/wgpu/resources/texture.rb +49 -12
  56. data/lib/wgpu/resources/texture_view.rb +25 -5
  57. data/lib/wgpu/texture_format.rb +14 -0
  58. data/lib/wgpu/version.rb +1 -1
  59. data/lib/wgpu/window.rb +26 -0
  60. data/sig/wgpu.rbs +85 -5
  61. metadata +12 -4
@@ -1,9 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "monitor"
4
+
3
5
  module WGPU
4
6
  class Buffer
5
7
  attr_reader :handle, :size, :usage
6
8
 
9
+ # Creates a GPU buffer with the requested size and usage.
10
+ # @param device [Device] owning device
11
+ # @param size [Integer] buffer size in bytes
12
+ # @param usage [Symbol, Array<Symbol>, Integer] usage flags
13
+ # @param mapped_at_creation [Boolean] whether to expose an initial mapped range
14
+ # @raise [BufferError] if native validation or creation fails
7
15
  def initialize(device, label: nil, size:, usage:, mapped_at_creation: false)
8
16
  @device = device
9
17
  @size = size
@@ -11,10 +19,14 @@ module WGPU
11
19
  begin
12
20
  normalize_usage(usage)
13
21
  rescue ArgumentError => e
14
- raise BufferError, buffer_error_message(e.message, label)
22
+ raise ArgumentError, buffer_error_message(e.message, label)
15
23
  end
16
24
  @mapped = mapped_at_creation
17
25
  @map_state = mapped_at_creation ? :mapped : :unmapped
26
+ @map_generation = 0
27
+ @map_state_mutex = Monitor.new
28
+ @mapped_offset = 0
29
+ @mapped_size = mapped_at_creation ? size : 0
18
30
 
19
31
  desc, keepalive = build_descriptor(
20
32
  label:,
@@ -24,9 +36,9 @@ module WGPU
24
36
  )
25
37
  @descriptor_keepalive = keepalive
26
38
 
27
- device.push_error_scope(:validation)
28
- @handle = Native.wgpuDeviceCreateBuffer(device.handle, desc)
29
- error = device.pop_error_scope
39
+ error = device.send(:capture_error_scope) do
40
+ @handle = Native.wgpuDeviceCreateBuffer(NativeResource.checked_handle(device, expected_class: Device), desc)
41
+ end
30
42
  @descriptor_keepalive = nil
31
43
 
32
44
  if @handle.null? || (error[:type] && error[:type] != :no_error)
@@ -35,123 +47,213 @@ module WGPU
35
47
  end
36
48
  end
37
49
 
50
+ # Writes typed data through the device's default queue.
51
+ # @param data [Array, String, FFI::Pointer] source data
52
+ # @param offset [Integer] destination byte offset
53
+ # @param type [Symbol] source element type
54
+ # @return [void]
38
55
  def write(data, offset: 0, type: :f32)
39
56
  ptr, byte_size = DataTypes.to_pointer(data, type:)
40
57
  DataTypes.validate_alignment!(offset, 4, name: "offset")
41
58
  DataTypes.validate_alignment!(byte_size, 4, name: "data size")
42
- Native.wgpuQueueWriteBuffer(@device.queue.handle, @handle, offset, ptr, byte_size)
59
+ Native.wgpuQueueWriteBuffer(NativeResource.checked_handle(@device.queue, expected_class: Queue), @handle, offset, ptr, byte_size)
43
60
  end
44
61
 
62
+ # Returns a writable view of a mapped byte range.
63
+ # @return [BufferMappedRange]
64
+ # @raise [BufferError] if the buffer is not mapped or no range is available
45
65
  def mapped_range(offset: 0, size: nil)
46
- raise BufferError, "Buffer is not mapped" unless @mapped
66
+ map_state_mutex.synchronize do
67
+ raise BufferError, "Buffer is not mapped" unless @mapped
47
68
 
48
- size ||= @size - offset
49
- validate_map_range!(offset, size)
50
- ptr = Native.wgpuBufferGetMappedRange(@handle, offset, size)
51
- raise BufferError, "Failed to get mapped range" if ptr.null?
69
+ size ||= @size - offset
70
+ offset, size = validate_mapped_access!(offset, size)
71
+ ptr = Native.wgpuBufferGetMappedRange(@handle, offset, size)
72
+ raise BufferError, "Failed to get mapped range" if ptr.null?
52
73
 
53
- BufferMappedRange.new(ptr, size)
74
+ BufferMappedRange.new(ptr, size, buffer: self, generation: current_map_generation)
75
+ end
54
76
  end
55
77
 
78
+ # Returns a writable view of a mapped byte range.
79
+ # @return [BufferMappedRange]
56
80
  def get_mapped_range(offset: 0, size: nil)
57
81
  mapped_range(offset: offset, size: size)
58
82
  end
59
83
 
84
+ # Unmaps the buffer and invalidates its mapped ranges.
85
+ # @return [void]
60
86
  def unmap
61
- Native.wgpuBufferUnmap(@handle)
62
- @mapped = false
63
- @map_state = :unmapped
87
+ map_state_mutex.synchronize do
88
+ Native.wgpuBufferUnmap(@handle)
89
+ invalidate_map_state
90
+ end
64
91
  end
65
92
 
93
+ # Maps a range and waits for native completion.
94
+ # @param mode [Symbol, Integer] map access mode
95
+ # @param timeout [Numeric, nil] maximum wait time in seconds
96
+ # @return [Boolean] true when mapped
97
+ # @raise [BufferError] if mapping fails
66
98
  def map_sync(mode, offset: 0, size: nil, timeout: nil)
67
- status_holder, callback_token, future = begin_map_request(mode, offset: offset, size: size)
99
+ timeout = AsyncWaiter.normalize_timeout(timeout)
100
+ status_holder, _callback_token, future, generation =
101
+ begin_map_request(mode, offset: offset, size: size)
68
102
  wait_for_map(status_holder, future, timeout:)
69
- finalize_map(status_holder)
70
- ensure
71
- CallbackKeepalive.release(self, callback_token)
103
+ finalize_map(status_holder, generation)
72
104
  end
73
105
 
106
+ # Maps a range on a background task.
107
+ # @param mode [Symbol, Integer] map access mode
108
+ # @return [AsyncTask] task yielding true when mapped
74
109
  def map_async(mode, offset: 0, size: nil)
75
- status_holder, callback_token, future = begin_map_request(mode, offset: offset, size: size)
110
+ status_holder, _callback_token, future, generation =
111
+ begin_map_request(mode, offset: offset, size: size)
76
112
  AsyncTask.new do
77
113
  wait_for_map(status_holder, future)
78
- finalize_map(status_holder)
79
- ensure
80
- CallbackKeepalive.release(self, callback_token)
114
+ finalize_map(status_holder, generation)
81
115
  end
82
116
  end
83
117
 
118
+ # Copies bytes from a const mapped range.
119
+ # @return [String] mapped bytes
120
+ # @raise [BufferError] if the buffer is not mapped
84
121
  def read_mapped_data(offset: 0, size: nil)
85
- raise BufferError, "Buffer is not mapped" unless @mapped
122
+ map_state_mutex.synchronize do
123
+ raise BufferError, "Buffer is not mapped" unless @mapped
86
124
 
87
- size ||= @size - offset
88
- validate_map_range!(offset, size)
89
- ptr = Native.wgpuBufferGetConstMappedRange(@handle, offset, size)
90
- raise BufferError, "Failed to get mapped range" if ptr.null?
125
+ size ||= @size - offset
126
+ offset, size = validate_mapped_access!(offset, size)
127
+ ptr = Native.wgpuBufferGetConstMappedRange(@handle, offset, size)
128
+ raise BufferError, "Failed to get mapped range" if ptr.null?
91
129
 
92
- ptr.read_bytes(size)
130
+ ptr.read_bytes(size)
131
+ end
93
132
  end
94
133
 
134
+ # Reads mapped bytes, optionally decoding typed values.
135
+ # @param type [Symbol, nil] element type, or +nil+ for raw bytes
136
+ # @return [String, Array]
95
137
  def read_mapped(offset: 0, size: nil, type: nil)
96
138
  bytes = read_mapped_data(offset: offset, size: size)
97
139
  type ? DataTypes.unpack(bytes, type:) : bytes
98
140
  end
99
141
 
142
+ # Writes typed data into a mapped range.
143
+ # @param data [Array, String, FFI::Pointer] source data
144
+ # @param type [Symbol] source element type
145
+ # @return [void]
100
146
  def write_mapped(data, offset: 0, type: :f32)
101
- raise BufferError, "Buffer is not mapped" unless @mapped
147
+ map_state_mutex.synchronize do
148
+ raise BufferError, "Buffer is not mapped" unless @mapped
102
149
 
103
- ptr, byte_size = DataTypes.to_pointer(data, type:)
104
- validate_map_range!(offset, byte_size)
105
- target = Native.wgpuBufferGetMappedRange(@handle, offset, byte_size)
106
- raise BufferError, "Failed to get mapped range" if target.null?
150
+ ptr, byte_size = DataTypes.to_pointer(data, type:)
151
+ offset, byte_size = validate_mapped_access!(offset, byte_size)
152
+ target = Native.wgpuBufferGetMappedRange(@handle, offset, byte_size)
153
+ raise BufferError, "Failed to get mapped range" if target.null?
107
154
 
108
- target.put_bytes(0, ptr.read_bytes(byte_size))
155
+ target.put_bytes(0, ptr.read_bytes(byte_size))
156
+ end
109
157
  end
110
158
 
159
+ # Reads mapped bytes as 32-bit floating-point values.
160
+ #
161
+ # @param offset [Integer] byte offset in the buffer
162
+ # @param count [Integer, nil] values to read; defaults to the remaining range
163
+ # @return [Array<Float>] decoded values
164
+ # @raise [BufferError] if the buffer is not mapped
111
165
  def read_mapped_floats(offset: 0, count: nil)
112
166
  read_mapped_values(type: :f32, offset:, count:)
113
167
  end
114
168
 
169
+ # Reads mapped bytes as unsigned 32-bit integers.
170
+ #
171
+ # @param offset [Integer] byte offset in the buffer
172
+ # @param count [Integer, nil] values to read; defaults to the remaining range
173
+ # @return [Array<Integer>] decoded values
174
+ # @raise [BufferError] if the buffer is not mapped
115
175
  def read_mapped_uint32s(offset: 0, count: nil)
116
176
  read_mapped_values(type: :u32, offset:, count:)
117
177
  end
118
178
 
179
+ # Reads mapped bytes as signed 32-bit integers.
180
+ #
181
+ # @param offset [Integer] byte offset in the buffer
182
+ # @param count [Integer, nil] values to read; defaults to the remaining range
183
+ # @return [Array<Integer>] decoded values
184
+ # @raise [BufferError] if the buffer is not mapped
119
185
  def read_mapped_int32s(offset: 0, count: nil)
120
186
  read_mapped_values(type: :i32, offset:, count:)
121
187
  end
122
188
 
189
+ # Reads mapped bytes as 64-bit floating-point values.
190
+ #
191
+ # @param offset [Integer] byte offset in the buffer
192
+ # @param count [Integer, nil] values to read; defaults to the remaining range
193
+ # @return [Array<Float>] decoded values
194
+ # @raise [BufferError] if the buffer is not mapped
123
195
  def read_mapped_float64s(offset: 0, count: nil)
124
196
  read_mapped_values(type: :f64, offset:, count:)
125
197
  end
126
198
 
199
+ # Reads mapped bytes as unsigned 16-bit integers.
200
+ #
201
+ # @param offset [Integer] byte offset in the buffer
202
+ # @param count [Integer, nil] values to read; defaults to the remaining range
203
+ # @return [Array<Integer>] decoded values
204
+ # @raise [BufferError] if the buffer is not mapped
127
205
  def read_mapped_uint16s(offset: 0, count: nil)
128
206
  read_mapped_values(type: :u16, offset:, count:)
129
207
  end
130
208
 
209
+ # Reads mapped bytes as unsigned 8-bit integers.
210
+ #
211
+ # @param offset [Integer] byte offset in the buffer
212
+ # @param count [Integer, nil] values to read; defaults to the remaining range
213
+ # @return [Array<Integer>] decoded values
214
+ # @raise [BufferError] if the buffer is not mapped
131
215
  def read_mapped_uint8s(offset: 0, count: nil)
132
216
  read_mapped_values(type: :u8, offset:, count:)
133
217
  end
134
218
 
219
+ # Reads mapped values of the requested element type.
220
+ # @param type [Symbol] element type
221
+ # @param count [Integer, nil] number of values
222
+ # @return [Array]
135
223
  def read_mapped_values(type: :f32, offset: 0, count: nil)
136
224
  element_size = DataTypes.byte_size(type)
137
225
  size = count ? count * element_size : @size - offset
138
226
  DataTypes.unpack(read_mapped_data(offset:, size:), type:)
139
227
  end
140
228
 
229
+ # Returns the current mapping state.
230
+ # @return [Symbol]
141
231
  def map_state
142
- return @map_state unless Native.buffer_map_state_available?
232
+ return map_state_mutex.synchronize { @map_state } unless Native.buffer_map_state_available?
143
233
 
144
- Native.wgpuBufferGetMapState(@handle) || @map_state
234
+ Native.wgpuBufferGetMapState(@handle) || map_state_mutex.synchronize { @map_state }
145
235
  end
146
236
 
237
+ # Destroys the buffer's storage.
238
+ # @return [void]
147
239
  def destroy
148
- Native.wgpuBufferDestroy(@handle)
240
+ map_state_mutex.synchronize do
241
+ Native.wgpuBufferDestroy(@handle)
242
+ invalidate_map_state
243
+ end
149
244
  end
150
245
 
246
+ # Releases the native buffer handle.
247
+ #
248
+ # Calling this method more than once has no effect.
249
+ # @return [void]
151
250
  def release
152
- return if @handle.null?
153
- Native.wgpuBufferRelease(@handle)
154
- @handle = FFI::Pointer::NULL
251
+ map_state_mutex.synchronize do
252
+ return if @handle.null?
253
+ Native.wgpuBufferRelease(@handle)
254
+ @handle = FFI::Pointer::NULL
255
+ invalidate_map_state
256
+ end
155
257
  end
156
258
 
157
259
  private
@@ -169,15 +271,37 @@ module WGPU
169
271
 
170
272
  def begin_map_request(mode, offset:, size:)
171
273
  size ||= @size - offset
172
- validate_map_range!(offset, size)
274
+ offset, size = validate_map_range!(offset, size)
173
275
  mode_flag = Native::EnumHelper.coerce(Native::MapMode, mode, name: "map mode")
174
276
 
175
277
  status_holder = { done: false, status: nil, message: nil }
278
+ generation = map_state_mutex.synchronize do
279
+ raise BufferError, "Buffer is already mapped or mapping" if @mapped || @map_state == :pending
280
+
281
+ @map_generation = current_map_generation + 1
282
+ @mapped_offset = offset
283
+ @mapped_size = size
284
+ @map_state = :pending
285
+ @map_generation
286
+ end
287
+ callback_lifetime_release = device_callback_lifetime_lease
288
+ callback_token = nil
176
289
  callback = FFI::Function.new(:void, [:uint32, Native::StringView.by_value, :pointer, :pointer]) do |status, message, _userdata1, _userdata2|
177
- status_holder[:done] = true
178
- status_holder[:status] = Native::MapAsyncStatus[status]
179
- if message[:data] && !message[:data].null? && message[:length] > 0
180
- status_holder[:message] = message[:data].read_string(message[:length])
290
+ begin
291
+ status_holder[:status] = Native::MapAsyncStatus[status]
292
+ if message[:data] && !message[:data].null? && message[:length] > 0
293
+ status_holder[:message] = message[:data].read_string(message[:length])
294
+ end
295
+ map_state_mutex.synchronize do
296
+ if generation == current_map_generation && !@handle.null?
297
+ @mapped = status_holder[:status] == :success
298
+ @map_state = @mapped ? :mapped : :unmapped
299
+ end
300
+ end
301
+ status_holder[:done] = true
302
+ ensure
303
+ CallbackKeepalive.release(self, callback_token)
304
+ callback_lifetime_release.call
181
305
  end
182
306
  end
183
307
  callback_token = CallbackKeepalive.retain(self, callback)
@@ -189,10 +313,17 @@ module WGPU
189
313
  callback_info[:userdata1] = nil
190
314
  callback_info[:userdata2] = nil
191
315
 
192
- future = Native.wgpuBufferMapAsync(@handle, mode_flag, offset, size, callback_info)
193
- @map_state = :pending
316
+ future =
317
+ begin
318
+ Native.wgpuBufferMapAsync(@handle, mode_flag, offset, size, callback_info)
319
+ rescue StandardError
320
+ CallbackKeepalive.release(self, callback_token)
321
+ callback_lifetime_release.call
322
+ invalidate_map_state(generation)
323
+ raise
324
+ end
194
325
 
195
- [status_holder, callback_token, future]
326
+ [status_holder, callback_token, future, generation]
196
327
  end
197
328
 
198
329
  def wait_for_map(status_holder, future, timeout: nil)
@@ -205,19 +336,41 @@ module WGPU
205
336
  )
206
337
  end
207
338
 
208
- def finalize_map(status_holder)
339
+ def finalize_map(status_holder, generation)
209
340
  if status_holder[:status] == :success
210
- @mapped = true
211
- @map_state = :mapped
341
+ map_state_mutex.synchronize do
342
+ if generation == current_map_generation && !@handle.null?
343
+ @mapped = true
344
+ @map_state = :mapped
345
+ end
346
+ end
212
347
  true
213
348
  else
214
- @map_state = :unmapped
349
+ invalidate_map_state(generation)
215
350
  detail = status_holder[:message]
216
351
  base = "Failed to map buffer: #{status_holder[:status]}"
217
352
  raise BufferError, detail && !detail.empty? ? "#{base} (#{detail})" : base
218
353
  end
219
354
  end
220
355
 
356
+ def invalidate_map_state(generation = nil)
357
+ map_state_mutex.synchronize do
358
+ return if generation && generation != current_map_generation
359
+
360
+ @map_generation = current_map_generation + 1
361
+ @mapped = false
362
+ @map_state = :unmapped
363
+ end
364
+ end
365
+
366
+ def current_map_generation
367
+ @map_generation ||= 0
368
+ end
369
+
370
+ def map_state_mutex
371
+ @map_state_mutex ||= Monitor.new
372
+ end
373
+
221
374
  def normalize_usage(usage)
222
375
  Native::EnumHelper.coerce_flags(Native::BufferUsage, usage, name: "buffer usage")
223
376
  end
@@ -227,85 +380,203 @@ module WGPU
227
380
  "create buffer#{context}: #{message}"
228
381
  end
229
382
 
383
+ def with_mapped_generation(generation)
384
+ map_state_mutex.synchronize do
385
+ unless @mapped && !released? && generation == current_map_generation
386
+ raise BufferError, "Mapped range is no longer valid"
387
+ end
388
+
389
+ yield
390
+ end
391
+ end
392
+
393
+ def validate_mapped_access!(offset, size)
394
+ offset, size = validate_map_range!(offset, size)
395
+ if offset < @mapped_offset || offset + size > @mapped_offset + @mapped_size
396
+ raise BufferError, "Requested range is outside the current mapping"
397
+ end
398
+
399
+ [offset, size]
400
+ end
401
+
230
402
  def validate_map_range!(offset, size)
231
- DataTypes.validate_alignment!(offset, 8, name: "map offset")
232
- DataTypes.validate_alignment!(size, 4, name: "map size")
403
+ offset = DataTypes.validate_alignment!(offset, 8, name: "map offset")
404
+ size = DataTypes.validate_alignment!(size, 4, name: "map size")
405
+ if offset > @size || size > @size - offset
406
+ raise ArgumentError,
407
+ "mapped range (offset #{offset}, size #{size}) exceeds buffer size #{@size}"
408
+ end
409
+
410
+ [offset, size]
233
411
  end
234
412
  end
235
413
 
236
414
  class BufferMappedRange
237
- def initialize(pointer, size)
415
+ # Wraps a native mapped memory range.
416
+ # @param pointer [FFI::Pointer] start of mapped memory
417
+ # @param size [Integer] range size in bytes
418
+ # @param buffer [Buffer] owner retaining the mapped allocation
419
+ # @param generation [Integer] mapping generation at acquisition
420
+ def initialize(pointer, size, buffer:, generation:)
238
421
  @pointer = pointer
239
422
  @size = size
423
+ @buffer = buffer
424
+ @generation = generation
240
425
  end
241
426
 
427
+ # Reads 32-bit floating-point values from the mapped range.
428
+ #
429
+ # @param count [Integer, nil] values to read; defaults to the full range
430
+ # @return [Array<Float>] decoded values
242
431
  def read_floats(count = nil)
243
432
  read(type: :f32, count:)
244
433
  end
245
434
 
435
+ # Writes 32-bit floating-point values into the mapped range.
436
+ #
437
+ # @param data [Array<Numeric>] values to write
438
+ # @return [void]
439
+ # @raise [ArgumentError] if the data exceeds the mapped range
246
440
  def write_floats(data)
247
441
  write(data, type: :f32)
248
442
  end
249
443
 
444
+ # Reads unsigned 32-bit integers from the mapped range.
445
+ #
446
+ # @param count [Integer, nil] values to read; defaults to the full range
447
+ # @return [Array<Integer>] decoded values
250
448
  def read_uint32s(count = nil)
251
449
  read(type: :u32, count:)
252
450
  end
253
451
 
452
+ # Writes unsigned 32-bit integers into the mapped range.
453
+ #
454
+ # @param data [Array<Integer>] values to write
455
+ # @return [void]
456
+ # @raise [ArgumentError] if the data exceeds the mapped range
254
457
  def write_uint32s(data)
255
458
  write(data, type: :u32)
256
459
  end
257
460
 
461
+ # Reads signed 32-bit integers from the mapped range.
462
+ #
463
+ # @param count [Integer, nil] values to read; defaults to the full range
464
+ # @return [Array<Integer>] decoded values
258
465
  def read_int32s(count = nil)
259
466
  read(type: :i32, count:)
260
467
  end
261
468
 
469
+ # Writes signed 32-bit integers into the mapped range.
470
+ #
471
+ # @param data [Array<Integer>] values to write
472
+ # @return [void]
473
+ # @raise [ArgumentError] if the data exceeds the mapped range
262
474
  def write_int32s(data)
263
475
  write(data, type: :i32)
264
476
  end
265
477
 
478
+ # Reads 64-bit floating-point values from the mapped range.
479
+ #
480
+ # @param count [Integer, nil] values to read; defaults to the full range
481
+ # @return [Array<Float>] decoded values
266
482
  def read_float64s(count = nil)
267
483
  read(type: :f64, count:)
268
484
  end
269
485
 
486
+ # Writes 64-bit floating-point values into the mapped range.
487
+ #
488
+ # @param data [Array<Numeric>] values to write
489
+ # @return [void]
490
+ # @raise [ArgumentError] if the data exceeds the mapped range
270
491
  def write_float64s(data)
271
492
  write(data, type: :f64)
272
493
  end
273
494
 
495
+ # Reads unsigned 16-bit integers from the mapped range.
496
+ #
497
+ # @param count [Integer, nil] values to read; defaults to the full range
498
+ # @return [Array<Integer>] decoded values
274
499
  def read_uint16s(count = nil)
275
500
  read(type: :u16, count:)
276
501
  end
277
502
 
503
+ # Writes unsigned 16-bit integers into the mapped range.
504
+ #
505
+ # @param data [Array<Integer>] values to write
506
+ # @return [void]
507
+ # @raise [ArgumentError] if the data exceeds the mapped range
278
508
  def write_uint16s(data)
279
509
  write(data, type: :u16)
280
510
  end
281
511
 
512
+ # Reads unsigned 8-bit integers from the mapped range.
513
+ #
514
+ # @param count [Integer, nil] values to read; defaults to the full range
515
+ # @return [Array<Integer>] decoded values
282
516
  def read_uint8s(count = nil)
283
517
  read(type: :u8, count:)
284
518
  end
285
519
 
520
+ # Writes unsigned 8-bit integers into the mapped range.
521
+ #
522
+ # @param data [Array<Integer>] values to write
523
+ # @return [void]
524
+ # @raise [ArgumentError] if the data exceeds the mapped range
286
525
  def write_uint8s(data)
287
526
  write(data, type: :u8)
288
527
  end
289
528
 
529
+ # Decodes typed values from the mapped range.
530
+ # @param type [Symbol] element type
531
+ # @param count [Integer, nil] number of values
532
+ # @return [Array]
290
533
  def read(type: :f32, count: nil)
291
534
  byte_size = DataTypes.byte_size(type)
292
535
  count ||= @size / byte_size
293
- DataTypes.unpack(@pointer.read_bytes(count * byte_size), type:)
536
+ count = Integer(count)
537
+ raise ArgumentError, "count must be non-negative" if count.negative?
538
+
539
+ bytes_to_read = count * byte_size
540
+ validate_byte_length!(bytes_to_read)
541
+ DataTypes.unpack(with_valid_mapping { @pointer.read_bytes(bytes_to_read) }, type:)
294
542
  end
295
543
 
544
+ # Encodes typed values into the mapped range.
545
+ # @param data [Array, String] values or bytes to write
546
+ # @param type [Symbol] element type
547
+ # @return [void]
548
+ # @raise [ArgumentError] if the encoded data exceeds the range
296
549
  def write(data, type: :f32)
297
550
  bytes = data.is_a?(String) ? data : DataTypes.pack(data, type:)
298
551
  raise ArgumentError, "data exceeds mapped range" if bytes.bytesize > @size
299
552
 
300
- @pointer.put_bytes(0, bytes)
553
+ with_valid_mapping { @pointer.put_bytes(0, bytes) }
301
554
  end
302
555
 
556
+ # Returns all bytes in the mapped range.
557
+ # @return [String]
303
558
  def read_bytes
304
- @pointer.read_bytes(@size)
559
+ with_valid_mapping { @pointer.read_bytes(@size) }
305
560
  end
306
561
 
562
+ # Writes bytes at the start of the mapped range.
563
+ # @param data [String] bytes to write
564
+ # @return [void]
307
565
  def write_bytes(data)
308
- @pointer.put_bytes(0, data)
566
+ validate_byte_length!(data.bytesize)
567
+ with_valid_mapping { @pointer.put_bytes(0, data) }
568
+ end
569
+
570
+ private
571
+
572
+ def with_valid_mapping(&block)
573
+ @buffer.send(:with_mapped_generation, @generation, &block)
574
+ end
575
+
576
+ def validate_byte_length!(byte_length)
577
+ return if byte_length <= @size
578
+
579
+ raise ArgumentError, "data exceeds mapped range"
309
580
  end
310
581
  end
311
582
  end
@@ -4,6 +4,11 @@ module WGPU
4
4
  class QuerySet
5
5
  attr_reader :handle, :count, :type
6
6
 
7
+ # Creates a set of GPU queries.
8
+ # @param device [Device] owning device
9
+ # @param type [Symbol, Integer] query type
10
+ # @param count [Integer] number of queries
11
+ # @raise [ResourceError] if native creation fails
7
12
  def initialize(device, label: nil, type:, count:)
8
13
  @device = device
9
14
  @count = Integer(count)
@@ -24,10 +29,12 @@ module WGPU
24
29
  desc[:type] = type_value
25
30
  desc[:count] = @count
26
31
 
27
- @handle = Native.wgpuDeviceCreateQuerySet(device.handle, desc)
32
+ @handle = Native.wgpuDeviceCreateQuerySet(NativeResource.checked_handle(device, expected_class: Device), desc)
28
33
  raise ResourceError, "Failed to create query set" if @handle.null?
29
34
  end
30
35
 
36
+ # Destroys the query storage once.
37
+ # @return [void]
31
38
  def destroy
32
39
  return if @destroyed
33
40
 
@@ -35,6 +42,11 @@ module WGPU
35
42
  @destroyed = true
36
43
  end
37
44
 
45
+ # Releases the native query set handle.
46
+ #
47
+ # A query set destroyed through {#destroy} is only marked released because
48
+ # the pinned native implementation cannot safely release it afterward.
49
+ # @return [void]
38
50
  def release
39
51
  return if @handle.null?
40
52
 
@@ -4,6 +4,9 @@ module WGPU
4
4
  class Sampler
5
5
  attr_reader :handle
6
6
 
7
+ # Creates a texture sampler with address, filter, and comparison settings.
8
+ # @param device [Device] owning device
9
+ # @raise [ResourceError] if native validation or creation fails
7
10
  def initialize(device, label: nil, address_mode_u: :clamp_to_edge, address_mode_v: :clamp_to_edge, address_mode_w: :clamp_to_edge, mag_filter: :nearest, min_filter: :nearest, mipmap_filter: :nearest, lod_min_clamp: 0.0, lod_max_clamp: 32.0, compare: nil, max_anisotropy: 1)
8
11
  @device = device
9
12
 
@@ -22,9 +25,9 @@ module WGPU
22
25
  )
23
26
  @descriptor_keepalive = keepalive
24
27
 
25
- device.push_error_scope(:validation)
26
- @handle = Native.wgpuDeviceCreateSampler(device.handle, desc)
27
- error = device.pop_error_scope
28
+ error = device.send(:capture_error_scope) do
29
+ @handle = Native.wgpuDeviceCreateSampler(NativeResource.checked_handle(device, expected_class: Device), desc)
30
+ end
28
31
  @descriptor_keepalive = nil
29
32
 
30
33
  if @handle.null? || (error[:type] && error[:type] != :no_error)
@@ -33,6 +36,10 @@ module WGPU
33
36
  end
34
37
  end
35
38
 
39
+ # Releases the native sampler handle.
40
+ #
41
+ # Calling this method more than once has no effect.
42
+ # @return [void]
36
43
  def release
37
44
  return if @handle.null?
38
45
  Native.wgpuSamplerRelease(@handle)