wgpu 1.1.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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +88 -0
  3. data/README.md +44 -5
  4. data/docs/README.md +22 -0
  5. data/docs/api_coverage.md +140 -0
  6. data/docs/async.md +41 -0
  7. data/docs/bind_groups.md +25 -0
  8. data/docs/buffer_data.md +37 -0
  9. data/docs/command_encoding.md +49 -0
  10. data/docs/errors.md +40 -0
  11. data/docs/getting_started_compute.md +62 -0
  12. data/docs/getting_started_rendering.md +62 -0
  13. data/docs/installation.md +94 -0
  14. data/docs/pipeline_descriptors.md +43 -0
  15. data/docs/releasing.md +32 -0
  16. data/docs/resource_lifetime.md +108 -0
  17. data/docs/shaders.md +32 -0
  18. data/docs/texture_readback.md +46 -0
  19. data/docs/troubleshooting.md +53 -0
  20. data/docs/upgrading_wgpu_native.md +37 -0
  21. data/ext/wgpu/extconf.rb +10 -142
  22. data/lib/wgpu/async_task.rb +19 -0
  23. data/lib/wgpu/commands/command_buffer.rb +25 -1
  24. data/lib/wgpu/commands/command_encoder.rb +123 -9
  25. data/lib/wgpu/commands/compute_pass.rb +53 -0
  26. data/lib/wgpu/commands/render_bundle.rb +9 -1
  27. data/lib/wgpu/commands/render_bundle_encoder.rb +65 -4
  28. data/lib/wgpu/commands/render_pass.rb +136 -8
  29. data/lib/wgpu/core/adapter.rb +123 -19
  30. data/lib/wgpu/core/async_waiter.rb +47 -4
  31. data/lib/wgpu/core/canvas_context.rb +32 -2
  32. data/lib/wgpu/core/device.rb +399 -53
  33. data/lib/wgpu/core/instance.rb +28 -4
  34. data/lib/wgpu/core/queue.rb +197 -51
  35. data/lib/wgpu/core/surface.rb +64 -17
  36. data/lib/wgpu/data_types.rb +83 -0
  37. data/lib/wgpu/descriptor_helpers.rb +104 -0
  38. data/lib/wgpu/error.rb +70 -0
  39. data/lib/wgpu/logging.rb +63 -0
  40. data/lib/wgpu/native/abi_verifier.rb +143 -0
  41. data/lib/wgpu/native/callbacks.rb +10 -1
  42. data/lib/wgpu/native/capabilities.rb +32 -2
  43. data/lib/wgpu/native/distribution.rb +176 -0
  44. data/lib/wgpu/native/enum_helper.rb +80 -0
  45. data/lib/wgpu/native/enums.rb +68 -8
  46. data/lib/wgpu/native/fixtures/webgpu-v27.0.4.0-enums.h +848 -0
  47. data/lib/wgpu/native/functions.rb +21 -10
  48. data/lib/wgpu/native/installer.rb +223 -0
  49. data/lib/wgpu/native/loader.rb +61 -21
  50. data/lib/wgpu/native/structs.rb +18 -1
  51. data/lib/wgpu/native_resource.rb +342 -0
  52. data/lib/wgpu/pipeline/bind_group.rb +21 -0
  53. data/lib/wgpu/pipeline/bind_group_layout.rb +108 -41
  54. data/lib/wgpu/pipeline/compute_pipeline.rb +40 -50
  55. data/lib/wgpu/pipeline/pipeline_layout.rb +8 -0
  56. data/lib/wgpu/pipeline/render_pipeline.rb +207 -110
  57. data/lib/wgpu/pipeline/shader_module.rb +95 -28
  58. data/lib/wgpu/resources/buffer.rb +387 -85
  59. data/lib/wgpu/resources/query_set.rb +26 -12
  60. data/lib/wgpu/resources/sampler.rb +43 -20
  61. data/lib/wgpu/resources/texture.rb +89 -48
  62. data/lib/wgpu/resources/texture_view.rb +35 -7
  63. data/lib/wgpu/texture_format.rb +96 -0
  64. data/lib/wgpu/version.rb +1 -1
  65. data/lib/wgpu/window.rb +34 -1
  66. data/lib/wgpu.rb +32 -0
  67. data/sig/wgpu.rbs +460 -0
  68. metadata +33 -16
@@ -4,148 +4,290 @@ 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
10
- @usage = normalize_usage(usage)
16
+ @usage =
17
+ begin
18
+ normalize_usage(usage)
19
+ rescue ArgumentError => e
20
+ raise ArgumentError, buffer_error_message(e.message, label)
21
+ end
11
22
  @mapped = mapped_at_creation
12
- @map_callbacks = []
13
-
14
- desc = Native::BufferDescriptor.new
15
- desc[:next_in_chain] = nil
16
- if label
17
- label_ptr = FFI::MemoryPointer.from_string(label)
18
- desc[:label][:data] = label_ptr
19
- desc[:label][:length] = label.bytesize
20
- else
21
- desc[:label][:data] = nil
22
- desc[:label][:length] = 0
23
- end
24
- desc[:usage] = @usage
25
- desc[:size] = size
26
- desc[:mapped_at_creation] = mapped_at_creation ? 1 : 0
23
+ @map_state = mapped_at_creation ? :mapped : :unmapped
24
+ @map_generation = 0
25
+ @map_state_mutex = Mutex.new
26
+
27
+ desc, keepalive = build_descriptor(
28
+ label:,
29
+ size:,
30
+ usage: @usage,
31
+ mapped_at_creation:
32
+ )
33
+ @descriptor_keepalive = keepalive
27
34
 
28
35
  device.push_error_scope(:validation)
29
36
  @handle = Native.wgpuDeviceCreateBuffer(device.handle, desc)
30
37
  error = device.pop_error_scope
38
+ @descriptor_keepalive = nil
31
39
 
32
40
  if @handle.null? || (error[:type] && error[:type] != :no_error)
33
41
  msg = error[:message] || "Failed to create buffer"
34
- raise BufferError, msg
42
+ raise BufferError, buffer_error_message(msg, label)
35
43
  end
36
44
  end
37
45
 
38
- def write(data, offset: 0)
39
- ptr, byte_size = data_to_pointer(data)
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]
51
+ def write(data, offset: 0, type: :f32)
52
+ ptr, byte_size = DataTypes.to_pointer(data, type:)
53
+ DataTypes.validate_alignment!(offset, 4, name: "offset")
54
+ DataTypes.validate_alignment!(byte_size, 4, name: "data size")
40
55
  Native.wgpuQueueWriteBuffer(@device.queue.handle, @handle, offset, ptr, byte_size)
41
56
  end
42
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
43
61
  def mapped_range(offset: 0, size: nil)
44
62
  raise BufferError, "Buffer is not mapped" unless @mapped
45
63
 
46
64
  size ||= @size - offset
65
+ offset, size = validate_map_range!(offset, size)
47
66
  ptr = Native.wgpuBufferGetMappedRange(@handle, offset, size)
48
67
  raise BufferError, "Failed to get mapped range" if ptr.null?
49
68
 
50
69
  BufferMappedRange.new(ptr, size)
51
70
  end
52
71
 
72
+ # Returns a writable view of a mapped byte range.
73
+ # @return [BufferMappedRange]
53
74
  def get_mapped_range(offset: 0, size: nil)
54
75
  mapped_range(offset: offset, size: size)
55
76
  end
56
77
 
78
+ # Unmaps the buffer and invalidates its mapped ranges.
79
+ # @return [void]
57
80
  def unmap
58
81
  Native.wgpuBufferUnmap(@handle)
59
- @mapped = false
82
+ map_state_mutex.synchronize do
83
+ @map_generation = current_map_generation + 1
84
+ @mapped = false
85
+ @map_state = :unmapped
86
+ end
60
87
  end
61
88
 
62
- def map_sync(mode, offset: 0, size: nil)
63
- status_holder, callback, future = begin_map_request(mode, offset: offset, size: size)
64
- wait_for_map(status_holder, future)
65
- finalize_map(status_holder)
66
- ensure
67
- @map_callbacks.delete(callback) if callback
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
94
+ def map_sync(mode, offset: 0, size: nil, timeout: nil)
95
+ status_holder, _callback_token, future, generation =
96
+ begin_map_request(mode, offset: offset, size: size)
97
+ wait_for_map(status_holder, future, timeout:)
98
+ finalize_map(status_holder, generation)
68
99
  end
69
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
70
104
  def map_async(mode, offset: 0, size: nil)
71
- status_holder, callback, 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)
72
107
  AsyncTask.new do
73
108
  wait_for_map(status_holder, future)
74
- finalize_map(status_holder)
75
- ensure
76
- @map_callbacks.delete(callback)
109
+ finalize_map(status_holder, generation)
77
110
  end
78
111
  end
79
112
 
113
+ # Copies bytes from a const mapped range.
114
+ # @return [String] mapped bytes
115
+ # @raise [BufferError] if the buffer is not mapped
80
116
  def read_mapped_data(offset: 0, size: nil)
81
117
  raise BufferError, "Buffer is not mapped" unless @mapped
82
118
 
83
119
  size ||= @size - offset
120
+ offset, size = validate_map_range!(offset, size)
84
121
  ptr = Native.wgpuBufferGetConstMappedRange(@handle, offset, size)
85
122
  raise BufferError, "Failed to get mapped range" if ptr.null?
86
123
 
87
124
  ptr.read_bytes(size)
88
125
  end
89
126
 
90
- def read_mapped(offset: 0, size: nil)
91
- read_mapped_data(offset: offset, size: size)
127
+ # Reads mapped bytes, optionally decoding typed values.
128
+ # @param type [Symbol, nil] element type, or +nil+ for raw bytes
129
+ # @return [String, Array]
130
+ def read_mapped(offset: 0, size: nil, type: nil)
131
+ bytes = read_mapped_data(offset: offset, size: size)
132
+ type ? DataTypes.unpack(bytes, type:) : bytes
92
133
  end
93
134
 
94
- def write_mapped(data, offset: 0)
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]
139
+ def write_mapped(data, offset: 0, type: :f32)
95
140
  raise BufferError, "Buffer is not mapped" unless @mapped
96
141
 
97
- ptr, byte_size = data_to_pointer(data)
142
+ ptr, byte_size = DataTypes.to_pointer(data, type:)
143
+ offset, byte_size = validate_map_range!(offset, byte_size)
98
144
  target = Native.wgpuBufferGetMappedRange(@handle, offset, byte_size)
99
145
  raise BufferError, "Failed to get mapped range" if target.null?
100
146
 
101
147
  target.put_bytes(0, ptr.read_bytes(byte_size))
102
148
  end
103
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
104
156
  def read_mapped_floats(offset: 0, count: nil)
105
- raise BufferError, "Buffer is not mapped" unless @mapped
157
+ read_mapped_values(type: :f32, offset:, count:)
158
+ end
106
159
 
107
- size = count ? count * 4 : @size - offset
108
- ptr = Native.wgpuBufferGetConstMappedRange(@handle, offset, size)
109
- raise BufferError, "Failed to get mapped range" if ptr.null?
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
166
+ def read_mapped_uint32s(offset: 0, count: nil)
167
+ read_mapped_values(type: :u32, offset:, count:)
168
+ end
110
169
 
111
- ptr.read_array_of_float(size / 4)
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
176
+ def read_mapped_int32s(offset: 0, count: nil)
177
+ read_mapped_values(type: :i32, offset:, count:)
112
178
  end
113
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
186
+ def read_mapped_float64s(offset: 0, count: nil)
187
+ read_mapped_values(type: :f64, offset:, count:)
188
+ end
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
196
+ def read_mapped_uint16s(offset: 0, count: nil)
197
+ read_mapped_values(type: :u16, offset:, count:)
198
+ end
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
206
+ def read_mapped_uint8s(offset: 0, count: nil)
207
+ read_mapped_values(type: :u8, offset:, count:)
208
+ end
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]
214
+ def read_mapped_values(type: :f32, offset: 0, count: nil)
215
+ element_size = DataTypes.byte_size(type)
216
+ size = count ? count * element_size : @size - offset
217
+ DataTypes.unpack(read_mapped_data(offset:, size:), type:)
218
+ end
219
+
220
+ # Returns the current mapping state.
221
+ # @return [Symbol]
114
222
  def map_state
115
- state = Native.wgpuBufferGetMapState(@handle)
116
- state || (@mapped ? :mapped : :unmapped)
223
+ return map_state_mutex.synchronize { @map_state } unless Native.buffer_map_state_available?
224
+
225
+ Native.wgpuBufferGetMapState(@handle) || map_state_mutex.synchronize { @map_state }
117
226
  end
118
227
 
228
+ # Destroys the buffer's storage.
229
+ # @return [void]
119
230
  def destroy
120
231
  Native.wgpuBufferDestroy(@handle)
232
+ invalidate_map_state
121
233
  end
122
234
 
235
+ # Releases the native buffer handle.
236
+ #
237
+ # Calling this method more than once has no effect.
238
+ # @return [void]
123
239
  def release
124
240
  return if @handle.null?
125
241
  Native.wgpuBufferRelease(@handle)
126
242
  @handle = FFI::Pointer::NULL
243
+ invalidate_map_state
127
244
  end
128
245
 
129
246
  private
130
247
 
248
+ def build_descriptor(label:, size:, usage:, mapped_at_creation:)
249
+ keepalive = []
250
+ desc = Native::BufferDescriptor.new
251
+ desc[:next_in_chain] = nil
252
+ DescriptorHelpers.set_label(desc, label, keepalive:)
253
+ desc[:usage] = usage
254
+ desc[:size] = size
255
+ desc[:mapped_at_creation] = mapped_at_creation ? 1 : 0
256
+ [desc, keepalive]
257
+ end
258
+
131
259
  def begin_map_request(mode, offset:, size:)
132
260
  size ||= @size - offset
133
- mode_flag = case mode
134
- when :read then Native::MapMode[:read]
135
- when :write then Native::MapMode[:write]
136
- when Integer then mode
137
- else raise ArgumentError, "Invalid map mode: #{mode}"
138
- end
261
+ offset, size = validate_map_range!(offset, size)
262
+ mode_flag = Native::EnumHelper.coerce(Native::MapMode, mode, name: "map mode")
139
263
 
140
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
141
272
  callback = FFI::Function.new(:void, [:uint32, Native::StringView.by_value, :pointer, :pointer]) do |status, message, _userdata1, _userdata2|
142
- status_holder[:done] = true
143
- status_holder[:status] = Native::MapAsyncStatus[status]
144
- if message[:data] && !message[:data].null? && message[:length] > 0
145
- 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
146
288
  end
147
289
  end
148
- @map_callbacks << callback
290
+ callback_token = CallbackKeepalive.retain(self, callback)
149
291
 
150
292
  callback_info = Native::BufferMapCallbackInfo.new
151
293
  callback_info[:next_in_chain] = nil
@@ -154,83 +296,243 @@ module WGPU
154
296
  callback_info[:userdata1] = nil
155
297
  callback_info[:userdata2] = nil
156
298
 
157
- future = Native.wgpuBufferMapAsync(@handle, mode_flag, offset, size, callback_info)
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
158
308
 
159
- [status_holder, callback, future]
309
+ [status_holder, callback_token, future, generation]
160
310
  end
161
311
 
162
- def wait_for_map(status_holder, future)
312
+ def wait_for_map(status_holder, future, timeout: nil)
163
313
  AsyncWaiter.wait(
164
314
  status_holder: status_holder,
165
315
  instance: @device.adapter&.instance,
166
316
  device: @device,
167
- future: future
317
+ future: future,
318
+ timeout: timeout
168
319
  )
169
320
  end
170
321
 
171
- def finalize_map(status_holder)
322
+ def finalize_map(status_holder, generation)
172
323
  if status_holder[:status] == :success
173
- @mapped = true
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
174
330
  true
175
331
  else
332
+ invalidate_map_state(generation)
176
333
  detail = status_holder[:message]
177
334
  base = "Failed to map buffer: #{status_holder[:status]}"
178
335
  raise BufferError, detail && !detail.empty? ? "#{base} (#{detail})" : base
179
336
  end
180
337
  end
181
338
 
182
- def normalize_usage(usage)
183
- case usage
184
- when Integer
185
- usage
186
- when Symbol
187
- Native::BufferUsage[usage]
188
- when Array
189
- usage.reduce(0) { |acc, u| acc | Native::BufferUsage[u] }
190
- else
191
- raise ArgumentError, "Invalid usage type: #{usage.class}"
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
192
346
  end
193
347
  end
194
348
 
195
- def data_to_pointer(data)
196
- case data
197
- when String
198
- ptr = FFI::MemoryPointer.new(:char, data.bytesize)
199
- ptr.put_bytes(0, data)
200
- [ptr, data.bytesize]
201
- when Array
202
- ptr = FFI::MemoryPointer.new(:float, data.size)
203
- ptr.write_array_of_float(data)
204
- [ptr, data.size * 4]
205
- when FFI::Pointer
206
- [data, data.size]
207
- else
208
- raise ArgumentError, "Unsupported data type: #{data.class}"
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
+
357
+ def normalize_usage(usage)
358
+ Native::EnumHelper.coerce_flags(Native::BufferUsage, usage, name: "buffer usage")
359
+ end
360
+
361
+ def buffer_error_message(message, label)
362
+ context = label ? " #{label.inspect}" : ""
363
+ "create buffer#{context}: #{message}"
364
+ end
365
+
366
+ def validate_map_range!(offset, 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}"
209
372
  end
373
+
374
+ [offset, size]
210
375
  end
211
376
  end
212
377
 
213
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
214
382
  def initialize(pointer, size)
215
383
  @pointer = pointer
216
384
  @size = size
217
385
  end
218
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
219
391
  def read_floats(count = nil)
220
- count ||= @size / 4
221
- @pointer.read_array_of_float(count)
392
+ read(type: :f32, count:)
222
393
  end
223
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
224
400
  def write_floats(data)
225
- @pointer.write_array_of_float(data)
401
+ write(data, type: :f32)
402
+ end
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
408
+ def read_uint32s(count = nil)
409
+ read(type: :u32, count:)
410
+ end
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
417
+ def write_uint32s(data)
418
+ write(data, type: :u32)
419
+ end
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
425
+ def read_int32s(count = nil)
426
+ read(type: :i32, count:)
427
+ end
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
434
+ def write_int32s(data)
435
+ write(data, type: :i32)
436
+ end
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
442
+ def read_float64s(count = nil)
443
+ read(type: :f64, count:)
444
+ end
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
451
+ def write_float64s(data)
452
+ write(data, type: :f64)
453
+ end
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
459
+ def read_uint16s(count = nil)
460
+ read(type: :u16, count:)
461
+ end
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
468
+ def write_uint16s(data)
469
+ write(data, type: :u16)
226
470
  end
227
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
476
+ def read_uint8s(count = nil)
477
+ read(type: :u8, count:)
478
+ end
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
485
+ def write_uint8s(data)
486
+ write(data, type: :u8)
487
+ end
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]
493
+ def read(type: :f32, count: nil)
494
+ byte_size = DataTypes.byte_size(type)
495
+ count ||= @size / byte_size
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:)
502
+ end
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
509
+ def write(data, type: :f32)
510
+ bytes = data.is_a?(String) ? data : DataTypes.pack(data, type:)
511
+ raise ArgumentError, "data exceeds mapped range" if bytes.bytesize > @size
512
+
513
+ @pointer.put_bytes(0, bytes)
514
+ end
515
+
516
+ # Returns all bytes in the mapped range.
517
+ # @return [String]
228
518
  def read_bytes
229
519
  @pointer.read_bytes(@size)
230
520
  end
231
521
 
522
+ # Writes bytes at the start of the mapped range.
523
+ # @param data [String] bytes to write
524
+ # @return [void]
232
525
  def write_bytes(data)
526
+ validate_byte_length!(data.bytesize)
233
527
  @pointer.put_bytes(0, data)
234
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
235
537
  end
236
538
  end
@@ -2,10 +2,19 @@
2
2
 
3
3
  module WGPU
4
4
  class QuerySet
5
- attr_reader :handle
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
14
+ @count = Integer(count)
15
+ type_value = Native::EnumHelper.coerce(Native::QueryType, type, name: "query type")
16
+ @type = Native::QueryType[type_value]
17
+ @destroyed = false
9
18
 
10
19
  desc = Native::QuerySetDescriptor.new
11
20
  desc[:next_in_chain] = nil
@@ -17,28 +26,33 @@ module WGPU
17
26
  desc[:label][:data] = nil
18
27
  desc[:label][:length] = 0
19
28
  end
20
- desc[:type] = type
21
- desc[:count] = count
29
+ desc[:type] = type_value
30
+ desc[:count] = @count
22
31
 
23
32
  @handle = Native.wgpuDeviceCreateQuerySet(device.handle, desc)
24
33
  raise ResourceError, "Failed to create query set" if @handle.null?
25
34
  end
26
35
 
27
- def count
28
- Native.wgpuQuerySetGetCount(@handle)
29
- end
30
-
31
- def type
32
- Native.wgpuQuerySetGetType(@handle)
33
- end
34
-
36
+ # Destroys the query storage once.
37
+ # @return [void]
35
38
  def destroy
39
+ return if @destroyed
40
+
36
41
  Native.wgpuQuerySetDestroy(@handle)
42
+ @destroyed = true
37
43
  end
38
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]
39
50
  def release
40
51
  return if @handle.null?
41
- Native.wgpuQuerySetRelease(@handle)
52
+
53
+ # Pinned wgpu-native v27 removes a destroyed query set from its storage;
54
+ # calling Release afterward aborts inside Rust instead of being harmless.
55
+ Native.wgpuQuerySetRelease(@handle) unless @destroyed
42
56
  @handle = FFI::Pointer::NULL
43
57
  end
44
58
  end