wgpu 1.2.0 → 1.2.1

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