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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +66 -0
- data/README.md +18 -2
- data/docs/README.md +2 -2
- data/docs/api_coverage.md +23 -15
- data/docs/async.md +13 -1
- data/docs/command_encoding.md +25 -0
- data/docs/errors.md +13 -0
- data/docs/getting_started_compute.md +1 -0
- data/docs/getting_started_rendering.md +5 -0
- data/docs/pipeline_descriptors.md +7 -2
- data/docs/releasing.md +11 -1
- data/docs/resource_lifetime.md +25 -2
- data/docs/texture_readback.md +27 -0
- data/docs/troubleshooting.md +5 -0
- data/docs/upgrading_wgpu_native.md +16 -5
- data/lib/wgpu/async_task.rb +19 -0
- data/lib/wgpu/commands/command_buffer.rb +14 -1
- data/lib/wgpu/commands/command_encoder.rb +82 -27
- data/lib/wgpu/commands/compute_pass.rb +49 -6
- data/lib/wgpu/commands/render_bundle.rb +9 -1
- data/lib/wgpu/commands/render_bundle_encoder.rb +63 -9
- data/lib/wgpu/commands/render_pass.rb +116 -14
- data/lib/wgpu/core/adapter.rb +95 -13
- data/lib/wgpu/core/async_waiter.rb +30 -4
- data/lib/wgpu/core/canvas_context.rb +37 -2
- data/lib/wgpu/core/device.rb +377 -70
- data/lib/wgpu/core/instance.rb +20 -0
- data/lib/wgpu/core/queue.rb +143 -43
- data/lib/wgpu/core/surface.rb +55 -7
- data/lib/wgpu/data_types.rb +16 -0
- data/lib/wgpu/descriptor_helpers.rb +87 -0
- data/lib/wgpu/error.rb +15 -0
- data/lib/wgpu/native/abi_verifier.rb +37 -3
- data/lib/wgpu/native/callbacks.rb +6 -0
- data/lib/wgpu/native/capabilities.rb +16 -2
- data/lib/wgpu/native/distribution.rb +63 -0
- data/lib/wgpu/native/enum_helper.rb +17 -0
- data/lib/wgpu/native/enums.rb +8 -0
- data/lib/wgpu/native/fixtures/webgpu-v27.0.4.0-enums.h +848 -0
- data/lib/wgpu/native/functions.rb +14 -0
- data/lib/wgpu/native/installer.rb +55 -18
- data/lib/wgpu/native/loader.rb +22 -0
- data/lib/wgpu/native/structs.rb +18 -1
- data/lib/wgpu/native_resource.rb +190 -3
- data/lib/wgpu/pipeline/bind_group.rb +16 -7
- data/lib/wgpu/pipeline/bind_group_layout.rb +20 -7
- data/lib/wgpu/pipeline/compute_pipeline.rb +25 -37
- data/lib/wgpu/pipeline/pipeline_layout.rb +12 -4
- data/lib/wgpu/pipeline/render_pipeline.rb +33 -48
- data/lib/wgpu/pipeline/shader_module.rb +57 -32
- data/lib/wgpu/resources/buffer.rb +330 -59
- data/lib/wgpu/resources/query_set.rb +13 -1
- data/lib/wgpu/resources/sampler.rb +10 -3
- data/lib/wgpu/resources/texture.rb +49 -12
- data/lib/wgpu/resources/texture_view.rb +25 -5
- data/lib/wgpu/texture_format.rb +14 -0
- data/lib/wgpu/version.rb +1 -1
- data/lib/wgpu/window.rb +26 -0
- data/sig/wgpu.rbs +85 -5
- metadata +12 -4
data/lib/wgpu/core/device.rb
CHANGED
|
@@ -1,29 +1,75 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
3
5
|
module WGPU
|
|
4
6
|
class Device
|
|
5
7
|
attr_reader :handle, :queue, :adapter
|
|
6
8
|
|
|
7
9
|
LIMIT_FIELDS = Native::Limits.members.freeze
|
|
8
10
|
|
|
11
|
+
# Requests a logical device and waits for the native callback.
|
|
12
|
+
#
|
|
13
|
+
# @param adapter [Adapter] adapter that will create the device
|
|
14
|
+
# @param label [String, nil] optional debugging label
|
|
15
|
+
# @param required_features [Array<Symbol, Integer>] features the device must enable
|
|
16
|
+
# @param required_limits [Hash, nil] minimum required limits
|
|
17
|
+
# @param timeout [Numeric, nil] maximum wait time in seconds
|
|
18
|
+
# @return [Device] requested device
|
|
19
|
+
# @raise [DeviceError] if device creation fails
|
|
20
|
+
# @raise [TimeoutError] if the request exceeds +timeout+
|
|
21
|
+
# @raise [ArgumentError] if +timeout+ is used without an instance-backed adapter
|
|
9
22
|
def self.request(adapter, label: nil, required_features: [], required_limits: nil, timeout: nil)
|
|
23
|
+
timeout = AsyncWaiter.normalize_timeout(timeout)
|
|
24
|
+
if timeout && !adapter.instance
|
|
25
|
+
raise ArgumentError,
|
|
26
|
+
"Device.request timeout requires an instance-backed adapter; " \
|
|
27
|
+
"pass instance: to Adapter.from_handle"
|
|
28
|
+
end
|
|
29
|
+
|
|
10
30
|
device_ptr = FFI::MemoryPointer.new(:pointer)
|
|
11
|
-
status_holder = {
|
|
31
|
+
status_holder = {
|
|
32
|
+
done: false,
|
|
33
|
+
value: nil,
|
|
34
|
+
message: nil,
|
|
35
|
+
abandoned: false,
|
|
36
|
+
cleanup_claimed: false,
|
|
37
|
+
mutex: Mutex.new
|
|
38
|
+
}
|
|
12
39
|
device_callback_state = {
|
|
13
40
|
mutex: Mutex.new,
|
|
14
41
|
uncaptured_error: nil,
|
|
15
42
|
device_lost: nil
|
|
16
43
|
}
|
|
17
44
|
|
|
45
|
+
callback_token = nil
|
|
18
46
|
callback = FFI::Function.new(
|
|
19
47
|
:void, [:uint32, :pointer, Native::StringView.by_value, :pointer, :pointer]
|
|
20
48
|
) do |status, device, message, _userdata1, _userdata2|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
status_holder[:
|
|
49
|
+
cleanup_abandoned = false
|
|
50
|
+
begin
|
|
51
|
+
status_holder[:mutex].synchronize do
|
|
52
|
+
status_holder[:value] = Native::RequestDeviceStatus[status]
|
|
53
|
+
if message[:data] && !message[:data].null? && message[:length] > 0
|
|
54
|
+
status_holder[:message] = message[:data].read_string(message[:length])
|
|
55
|
+
end
|
|
56
|
+
device_ptr.write_pointer(device)
|
|
57
|
+
status_holder[:done] = true
|
|
58
|
+
if status_holder[:abandoned] && !status_holder[:cleanup_claimed]
|
|
59
|
+
status_holder[:cleanup_claimed] = true
|
|
60
|
+
cleanup_abandoned = true
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
if cleanup_abandoned
|
|
64
|
+
if status_holder[:value] == :success && device && !device.null?
|
|
65
|
+
release_abandoned_device(device, device_callback_state)
|
|
66
|
+
else
|
|
67
|
+
release_device_callback_keepalive(device_callback_state)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
ensure
|
|
71
|
+
CallbackKeepalive.release(adapter, callback_token)
|
|
24
72
|
end
|
|
25
|
-
device_ptr.write_pointer(device)
|
|
26
|
-
status_holder[:done] = true
|
|
27
73
|
end
|
|
28
74
|
|
|
29
75
|
queue_desc = Native::QueueDescriptor.new
|
|
@@ -81,35 +127,90 @@ module WGPU
|
|
|
81
127
|
callback_info[:userdata1] = nil
|
|
82
128
|
callback_info[:userdata2] = nil
|
|
83
129
|
|
|
130
|
+
device_lost_token = CallbackKeepalive.retain(adapter, device_lost_callback)
|
|
131
|
+
uncaptured_error_token = CallbackKeepalive.retain(adapter, uncaptured_error_callback)
|
|
132
|
+
configure_device_callback_keepalive(
|
|
133
|
+
device_callback_state,
|
|
134
|
+
owner: adapter,
|
|
135
|
+
tokens: [device_lost_token, uncaptured_error_token]
|
|
136
|
+
)
|
|
84
137
|
callback_token = CallbackKeepalive.retain(adapter, callback)
|
|
138
|
+
future =
|
|
139
|
+
begin
|
|
140
|
+
Native.wgpuAdapterRequestDevice(NativeResource.checked_handle(adapter, expected_class: Adapter), desc, callback_info)
|
|
141
|
+
rescue StandardError
|
|
142
|
+
CallbackKeepalive.release(adapter, callback_token)
|
|
143
|
+
release_device_callback_keepalive(device_callback_state)
|
|
144
|
+
raise
|
|
145
|
+
end
|
|
146
|
+
|
|
85
147
|
begin
|
|
86
|
-
future = Native.wgpuAdapterRequestDevice(adapter.handle, desc, callback_info)
|
|
87
148
|
AsyncWaiter.wait(
|
|
88
149
|
status_holder: status_holder,
|
|
89
150
|
instance: adapter.instance,
|
|
90
151
|
future: future,
|
|
91
152
|
timeout: timeout
|
|
92
153
|
)
|
|
93
|
-
|
|
94
|
-
|
|
154
|
+
rescue StandardError
|
|
155
|
+
abandoned_device = nil
|
|
156
|
+
cleanup_abandoned = status_holder[:mutex].synchronize do
|
|
157
|
+
status_holder[:abandoned] = true
|
|
158
|
+
next false unless status_holder[:done] && !status_holder[:cleanup_claimed]
|
|
159
|
+
|
|
160
|
+
status_holder[:cleanup_claimed] = true
|
|
161
|
+
abandoned_device = device_ptr.read_pointer
|
|
162
|
+
true
|
|
163
|
+
end
|
|
164
|
+
if cleanup_abandoned
|
|
165
|
+
if status_holder[:value] == :success && abandoned_device && !abandoned_device.null?
|
|
166
|
+
release_abandoned_device(abandoned_device, device_callback_state)
|
|
167
|
+
else
|
|
168
|
+
release_device_callback_keepalive(device_callback_state)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
raise
|
|
95
172
|
end
|
|
96
173
|
|
|
97
174
|
handle = device_ptr.read_pointer
|
|
98
175
|
if handle.null? || status_holder[:value] != :success
|
|
176
|
+
release_device_callback_keepalive(device_callback_state)
|
|
99
177
|
msg = status_holder[:message] || "Unknown error"
|
|
100
178
|
raise DeviceError, "Failed to request device: #{msg}"
|
|
101
179
|
end
|
|
102
180
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
181
|
+
begin
|
|
182
|
+
callback_lifetime = DeviceCallbackLifetime.new do
|
|
183
|
+
release_device_callback_keepalive(device_callback_state)
|
|
184
|
+
end
|
|
185
|
+
device_callback_state[:mutex].synchronize do
|
|
186
|
+
device_callback_state[:callback_lifetime] = callback_lifetime
|
|
187
|
+
end
|
|
188
|
+
device = new(
|
|
189
|
+
handle,
|
|
190
|
+
adapter: adapter,
|
|
191
|
+
label: label,
|
|
192
|
+
callback_state: device_callback_state,
|
|
193
|
+
callback_lifetime: callback_lifetime
|
|
194
|
+
)
|
|
195
|
+
device.send(:adopt_device_callback_keepalive)
|
|
196
|
+
device
|
|
197
|
+
rescue StandardError
|
|
198
|
+
device&.release
|
|
199
|
+
release_device_callback_keepalive(device_callback_state)
|
|
200
|
+
raise
|
|
201
|
+
end
|
|
107
202
|
end
|
|
108
203
|
|
|
109
|
-
|
|
204
|
+
# Wraps a native device and obtains its default queue.
|
|
205
|
+
# @param handle [FFI::Pointer] native device handle
|
|
206
|
+
# @param adapter [Adapter, nil] adapter that created the device
|
|
207
|
+
# @param label [String, nil] optional debug label
|
|
208
|
+
# @param callback_lifetime [DeviceCallbackLifetime, nil] shared native callback lifetime
|
|
209
|
+
def initialize(handle, adapter: nil, label: nil, callback_state: nil, callback_lifetime: nil)
|
|
110
210
|
@handle = handle
|
|
111
211
|
@adapter = adapter
|
|
112
212
|
@label = label
|
|
213
|
+
@device_callback_lifetime = callback_lifetime
|
|
113
214
|
@device_callback_state = callback_state || {
|
|
114
215
|
mutex: Mutex.new,
|
|
115
216
|
uncaptured_error: nil,
|
|
@@ -119,14 +220,20 @@ module WGPU
|
|
|
119
220
|
@queue = Queue.new(Native.wgpuDeviceGetQueue(@handle), device: self)
|
|
120
221
|
end
|
|
121
222
|
|
|
223
|
+
# Returns information about the adapter that created this device.
|
|
224
|
+
# @return [Hash, nil]
|
|
122
225
|
def adapter_info
|
|
123
226
|
@adapter&.info
|
|
124
227
|
end
|
|
125
228
|
|
|
229
|
+
# Creates a GPU buffer.
|
|
230
|
+
# @return [Buffer]
|
|
126
231
|
def create_buffer(label: nil, size:, usage:, mapped_at_creation: false)
|
|
127
232
|
Buffer.new(self, label: label, size: size, usage: usage, mapped_at_creation: mapped_at_creation)
|
|
128
233
|
end
|
|
129
234
|
|
|
235
|
+
# Creates a shader module from WGSL or SPIR-V source.
|
|
236
|
+
# @return [ShaderModule]
|
|
130
237
|
def create_shader_module(label: nil, code: nil, spirv: nil, compilation_hints: [], validate: false)
|
|
131
238
|
ShaderModule.new(
|
|
132
239
|
self,
|
|
@@ -138,32 +245,46 @@ module WGPU
|
|
|
138
245
|
)
|
|
139
246
|
end
|
|
140
247
|
|
|
248
|
+
# Creates a command encoder.
|
|
249
|
+
# @return [CommandEncoder]
|
|
141
250
|
def create_command_encoder(label: nil)
|
|
142
251
|
CommandEncoder.new(self, label: label)
|
|
143
252
|
end
|
|
144
253
|
|
|
254
|
+
# Creates a bind group layout.
|
|
255
|
+
# @return [BindGroupLayout]
|
|
145
256
|
def create_bind_group_layout(label: nil, entries:)
|
|
146
257
|
BindGroupLayout.new(self, label: label, entries: entries)
|
|
147
258
|
end
|
|
148
259
|
|
|
260
|
+
# Creates a bind group.
|
|
261
|
+
# @return [BindGroup]
|
|
149
262
|
def create_bind_group(label: nil, layout:, entries:)
|
|
150
263
|
BindGroup.new(self, label: label, layout: layout, entries: entries)
|
|
151
264
|
end
|
|
152
265
|
|
|
266
|
+
# Creates a pipeline layout.
|
|
267
|
+
# @return [PipelineLayout]
|
|
153
268
|
def create_pipeline_layout(label: nil, bind_group_layouts:)
|
|
154
269
|
PipelineLayout.new(self, label: label, bind_group_layouts: bind_group_layouts)
|
|
155
270
|
end
|
|
156
271
|
|
|
272
|
+
# Creates a compute pipeline.
|
|
273
|
+
# @return [ComputePipeline]
|
|
157
274
|
def create_compute_pipeline(label: nil, layout:, compute:)
|
|
158
275
|
ComputePipeline.new(self, label: label, layout: layout, compute: compute)
|
|
159
276
|
end
|
|
160
277
|
|
|
278
|
+
# Creates a compute pipeline on a background task.
|
|
279
|
+
# @return [AsyncTask] task yielding a {ComputePipeline}
|
|
161
280
|
def create_compute_pipeline_async(label: nil, layout:, compute:)
|
|
162
281
|
AsyncTask.new do
|
|
163
282
|
create_compute_pipeline(label: label, layout: layout, compute: compute)
|
|
164
283
|
end
|
|
165
284
|
end
|
|
166
285
|
|
|
286
|
+
# Creates a render pipeline.
|
|
287
|
+
# @return [RenderPipeline]
|
|
167
288
|
def create_render_pipeline(label: nil, layout:, vertex:, primitive: {}, depth_stencil: nil, multisample: {}, fragment: nil)
|
|
168
289
|
RenderPipeline.new(self,
|
|
169
290
|
label: label,
|
|
@@ -176,6 +297,8 @@ module WGPU
|
|
|
176
297
|
)
|
|
177
298
|
end
|
|
178
299
|
|
|
300
|
+
# Creates a render pipeline on a background task.
|
|
301
|
+
# @return [AsyncTask] task yielding a {RenderPipeline}
|
|
179
302
|
def create_render_pipeline_async(label: nil, layout:, vertex:, primitive: {}, depth_stencil: nil, multisample: {}, fragment: nil)
|
|
180
303
|
AsyncTask.new do
|
|
181
304
|
create_render_pipeline(
|
|
@@ -190,6 +313,8 @@ module WGPU
|
|
|
190
313
|
end
|
|
191
314
|
end
|
|
192
315
|
|
|
316
|
+
# Creates a texture.
|
|
317
|
+
# @return [Texture]
|
|
193
318
|
def create_texture(label: nil, size:, format:, usage:, dimension: :d2, mip_level_count: 1, sample_count: 1, view_formats: [])
|
|
194
319
|
Texture.new(self,
|
|
195
320
|
label: label,
|
|
@@ -203,6 +328,8 @@ module WGPU
|
|
|
203
328
|
)
|
|
204
329
|
end
|
|
205
330
|
|
|
331
|
+
# Creates a texture sampler.
|
|
332
|
+
# @return [Sampler]
|
|
206
333
|
def create_sampler(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)
|
|
207
334
|
Sampler.new(self,
|
|
208
335
|
label: label,
|
|
@@ -219,6 +346,11 @@ module WGPU
|
|
|
219
346
|
)
|
|
220
347
|
end
|
|
221
348
|
|
|
349
|
+
# Creates a mapped buffer initialized from typed Ruby data.
|
|
350
|
+
# @param data [Array, String, FFI::Pointer] source data
|
|
351
|
+
# @param usage [Symbol, Array<Symbol>, Integer] buffer usage flags
|
|
352
|
+
# @param type [Symbol] source element type
|
|
353
|
+
# @return [Buffer]
|
|
222
354
|
def create_buffer_with_data(label: nil, data:, usage:, type: :f32)
|
|
223
355
|
data_ptr, byte_size = DataTypes.to_pointer(data, type:)
|
|
224
356
|
DataTypes.validate_alignment!(byte_size, 4, name: "buffer data size")
|
|
@@ -233,10 +365,14 @@ module WGPU
|
|
|
233
365
|
buffer
|
|
234
366
|
end
|
|
235
367
|
|
|
368
|
+
# Creates a GPU query set.
|
|
369
|
+
# @return [QuerySet]
|
|
236
370
|
def create_query_set(label: nil, type:, count:)
|
|
237
371
|
QuerySet.new(self, label: label, type: type, count: count)
|
|
238
372
|
end
|
|
239
373
|
|
|
374
|
+
# Creates an encoder for reusable render commands.
|
|
375
|
+
# @return [RenderBundleEncoder]
|
|
240
376
|
def create_render_bundle_encoder(color_formats:, depth_stencil_format: nil, sample_count: 1,
|
|
241
377
|
depth_read_only: false, stencil_read_only: false, label: nil)
|
|
242
378
|
RenderBundleEncoder.new(self,
|
|
@@ -249,6 +385,8 @@ module WGPU
|
|
|
249
385
|
)
|
|
250
386
|
end
|
|
251
387
|
|
|
388
|
+
# Lists optional features enabled on the device.
|
|
389
|
+
# @return [Array<Symbol>]
|
|
252
390
|
def features
|
|
253
391
|
supported = Native::SupportedFeatures.new
|
|
254
392
|
Native.wgpuDeviceGetFeatures(@handle, supported)
|
|
@@ -260,12 +398,19 @@ module WGPU
|
|
|
260
398
|
end
|
|
261
399
|
end
|
|
262
400
|
result
|
|
401
|
+
ensure
|
|
402
|
+
Native.wgpuSupportedFeaturesFreeMembers(supported) if supported
|
|
263
403
|
end
|
|
264
404
|
|
|
405
|
+
# Reports whether a feature is enabled on the device.
|
|
406
|
+
# @param feature [Symbol] feature name
|
|
407
|
+
# @return [Boolean]
|
|
265
408
|
def has_feature?(feature)
|
|
266
409
|
features.include?(feature)
|
|
267
410
|
end
|
|
268
411
|
|
|
412
|
+
# Returns resource limits supported by the device.
|
|
413
|
+
# @return [Hash{Symbol => Integer}]
|
|
269
414
|
def limits
|
|
270
415
|
supported = Native::SupportedLimits.new
|
|
271
416
|
supported[:next_in_chain] = nil
|
|
@@ -273,6 +418,9 @@ module WGPU
|
|
|
273
418
|
limits_to_hash(supported[:limits])
|
|
274
419
|
end
|
|
275
420
|
|
|
421
|
+
# Processes device work, optionally waiting for queue progress.
|
|
422
|
+
# @param wait [Boolean] whether to wait for submitted work
|
|
423
|
+
# @return [Integer] native poll status
|
|
276
424
|
def poll(wait: false)
|
|
277
425
|
if Native.device_poll_available?
|
|
278
426
|
Native.wgpuDevicePoll(@handle, wait ? 1 : 0, nil)
|
|
@@ -282,65 +430,68 @@ module WGPU
|
|
|
282
430
|
end
|
|
283
431
|
end
|
|
284
432
|
|
|
433
|
+
# Pushes a scope that captures matching GPU errors.
|
|
434
|
+
# Scopes belong to the pushing thread and serialize scoped work on this device.
|
|
435
|
+
# Pop on the same thread; do not wait here for another thread's scoped work.
|
|
436
|
+
# @param filter [Symbol, Integer] error filter
|
|
437
|
+
# @return [void]
|
|
285
438
|
def push_error_scope(filter = :validation)
|
|
286
439
|
filter_value = Native::EnumHelper.coerce(Native::ErrorFilter, filter, name: "error filter")
|
|
287
|
-
|
|
288
|
-
end
|
|
289
|
-
|
|
290
|
-
def pop_error_scope(timeout: nil)
|
|
291
|
-
error_holder = { done: false, status: nil, type: nil, message: nil }
|
|
292
|
-
|
|
293
|
-
callback = FFI::Function.new(
|
|
294
|
-
:void, [:uint32, :uint32, Native::StringView.by_value, :pointer, :pointer]
|
|
295
|
-
) do |status, error_type, message, _userdata1, _userdata2|
|
|
296
|
-
error_holder[:done] = true
|
|
297
|
-
error_holder[:status] = Native::PopErrorScopeStatus[status]
|
|
298
|
-
error_holder[:type] = Native::ErrorType[error_type]
|
|
299
|
-
if message[:data] && !message[:data].null? && message[:length] > 0
|
|
300
|
-
error_holder[:message] = message[:data].read_string(message[:length])
|
|
301
|
-
end
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
callback_info = Native::PopErrorScopeCallbackInfo.new
|
|
305
|
-
callback_info[:next_in_chain] = nil
|
|
306
|
-
callback_info[:mode] = AsyncWaiter.callback_mode(instance: @adapter&.instance)
|
|
307
|
-
callback_info[:callback] = callback
|
|
308
|
-
callback_info[:userdata1] = nil
|
|
309
|
-
callback_info[:userdata2] = nil
|
|
310
|
-
|
|
311
|
-
callback_token = CallbackKeepalive.retain(self, callback)
|
|
440
|
+
error_scope_monitor.enter
|
|
312
441
|
begin
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
)
|
|
321
|
-
ensure
|
|
322
|
-
CallbackKeepalive.release(self, callback_token)
|
|
442
|
+
ensure_not_released!
|
|
443
|
+
Native.wgpuDevicePushErrorScope(@handle, filter_value)
|
|
444
|
+
@error_scope_depth = (@error_scope_depth || 0) + 1
|
|
445
|
+
@error_scope_owner = Thread.current
|
|
446
|
+
rescue StandardError
|
|
447
|
+
error_scope_monitor.exit
|
|
448
|
+
raise
|
|
323
449
|
end
|
|
450
|
+
end
|
|
324
451
|
|
|
325
|
-
|
|
452
|
+
# Pops the latest error scope and waits for its result.
|
|
453
|
+
# @param timeout [Numeric, nil] maximum wait time in seconds
|
|
454
|
+
# @return [Hash] native status, error type, and message
|
|
455
|
+
def pop_error_scope(timeout: nil)
|
|
456
|
+
timeout = AsyncWaiter.normalize_timeout(timeout)
|
|
457
|
+
error_holder, future = begin_pop_error_scope
|
|
458
|
+
wait_for_error_scope(error_holder, future, timeout: timeout)
|
|
326
459
|
end
|
|
327
460
|
|
|
461
|
+
# Starts popping on the calling thread and waits for completion in a background task.
|
|
462
|
+
#
|
|
463
|
+
# @param timeout [Numeric, nil] maximum wait time in seconds
|
|
464
|
+
# @return [AsyncTask] task whose value is the error hash
|
|
328
465
|
def pop_error_scope_async(timeout: nil)
|
|
329
|
-
|
|
466
|
+
timeout = AsyncWaiter.normalize_timeout(timeout)
|
|
467
|
+
error_holder, future = begin_pop_error_scope
|
|
468
|
+
AsyncTask.new { wait_for_error_scope(error_holder, future, timeout: timeout) }
|
|
330
469
|
end
|
|
331
470
|
|
|
471
|
+
# Pops the latest error scope as a typed error.
|
|
472
|
+
# @param timeout [Numeric, nil] maximum wait time in seconds
|
|
473
|
+
# @return [GPUError, nil]
|
|
332
474
|
def pop_error_scope_typed(timeout: nil)
|
|
333
475
|
GPUError.from_hash(pop_error_scope(timeout: timeout))
|
|
334
476
|
end
|
|
335
477
|
|
|
478
|
+
# Runs a block inside an error scope and raises captured GPU errors.
|
|
479
|
+
# @param filter [Symbol, Integer] error filter
|
|
480
|
+
# @yieldreturn [Object] block result
|
|
481
|
+
# @return [Object] block result
|
|
336
482
|
def with_error_scope(filter = :validation)
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
error
|
|
483
|
+
raise ArgumentError, "block is required" unless block_given?
|
|
484
|
+
|
|
485
|
+
result = nil
|
|
486
|
+
error = capture_error_scope(filter) { result = yield }
|
|
487
|
+
GPUError.from_hash(error)&.raise!
|
|
341
488
|
result
|
|
342
489
|
end
|
|
343
490
|
|
|
491
|
+
# Registers the handler for uncaptured GPU errors.
|
|
492
|
+
# @yieldparam error [GPUError] reported error
|
|
493
|
+
# @return [Device] this device
|
|
494
|
+
# @raise [ArgumentError] if no block is provided
|
|
344
495
|
def on_uncaptured_error(&handler)
|
|
345
496
|
raise ArgumentError, "on_uncaptured_error requires a block" unless handler
|
|
346
497
|
|
|
@@ -348,6 +499,11 @@ module WGPU
|
|
|
348
499
|
self
|
|
349
500
|
end
|
|
350
501
|
|
|
502
|
+
# Registers the handler invoked when the device is lost.
|
|
503
|
+
# @yieldparam reason [Symbol] native device-lost reason
|
|
504
|
+
# @yieldparam message [String] native diagnostic message
|
|
505
|
+
# @return [Device] this device
|
|
506
|
+
# @raise [ArgumentError] if no block is provided
|
|
351
507
|
def on_device_lost(&handler)
|
|
352
508
|
raise ArgumentError, "on_device_lost requires a block" unless handler
|
|
353
509
|
|
|
@@ -355,18 +511,26 @@ module WGPU
|
|
|
355
511
|
self
|
|
356
512
|
end
|
|
357
513
|
|
|
514
|
+
# Destroys device-owned native resources.
|
|
515
|
+
# @return [void]
|
|
358
516
|
def destroy
|
|
359
517
|
return if @handle.null?
|
|
360
518
|
Native.wgpuDeviceDestroy(@handle)
|
|
361
519
|
end
|
|
362
520
|
|
|
521
|
+
# Releases the default queue and native device handle.
|
|
522
|
+
#
|
|
523
|
+
# Device callbacks remain alive until all derived wrappers and pending
|
|
524
|
+
# callback operations have also completed their native releases.
|
|
525
|
+
#
|
|
526
|
+
# Calling this method more than once has no effect.
|
|
527
|
+
# @return [void]
|
|
363
528
|
def release
|
|
364
529
|
@queue&.release
|
|
365
530
|
return if @handle.null?
|
|
366
531
|
Native.wgpuDeviceRelease(@handle)
|
|
367
532
|
@handle = FFI::Pointer::NULL
|
|
368
|
-
|
|
369
|
-
@device_callback_tokens&.clear
|
|
533
|
+
@device_callback_tokens = nil
|
|
370
534
|
end
|
|
371
535
|
|
|
372
536
|
def self.normalize_required_features(required_features)
|
|
@@ -416,12 +580,17 @@ module WGPU
|
|
|
416
580
|
FFI::Function.new(
|
|
417
581
|
:void, [:pointer, :uint32, Native::StringView.by_value, :pointer, :pointer]
|
|
418
582
|
) do |_device, type, message, _userdata1, _userdata2|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
583
|
+
callback_lifetime = retain_device_callback_lifetime(state)
|
|
584
|
+
begin
|
|
585
|
+
error = GPUError.new(
|
|
586
|
+
type: Native::ErrorType[type] || :unknown,
|
|
587
|
+
message: string_from_callback(message)
|
|
588
|
+
)
|
|
589
|
+
dispatch_device_callback(state, :uncaptured_error, error) do
|
|
590
|
+
warn "Uncaptured GPU error (#{error.type}): #{error.message}"
|
|
591
|
+
end
|
|
592
|
+
ensure
|
|
593
|
+
callback_lifetime&.release
|
|
425
594
|
end
|
|
426
595
|
end
|
|
427
596
|
end
|
|
@@ -430,14 +599,69 @@ module WGPU
|
|
|
430
599
|
FFI::Function.new(
|
|
431
600
|
:void, [:pointer, :uint32, Native::StringView.by_value, :pointer, :pointer]
|
|
432
601
|
) do |_device, reason, message, _userdata1, _userdata2|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
602
|
+
callback_lifetime = retain_device_callback_lifetime(state)
|
|
603
|
+
begin
|
|
604
|
+
reason_name = Native::DeviceLostReason[reason] || :unknown
|
|
605
|
+
message_text = string_from_callback(message)
|
|
606
|
+
dispatch_device_callback(state, :device_lost, reason_name, message_text) do
|
|
607
|
+
warn "GPU device lost (#{reason_name}): #{message_text}" unless reason_name == :destroyed
|
|
608
|
+
end
|
|
609
|
+
ensure
|
|
610
|
+
callback_lifetime&.release
|
|
437
611
|
end
|
|
438
612
|
end
|
|
439
613
|
end
|
|
440
614
|
|
|
615
|
+
def self.retain_device_callback_lifetime(state)
|
|
616
|
+
state[:mutex].synchronize do
|
|
617
|
+
lifetime = state[:callback_lifetime]
|
|
618
|
+
lifetime&.retain
|
|
619
|
+
lifetime
|
|
620
|
+
end
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
def self.configure_device_callback_keepalive(state, owner:, tokens:)
|
|
624
|
+
state[:mutex].synchronize do
|
|
625
|
+
state[:callback_owner] = owner
|
|
626
|
+
state[:callback_tokens] = tokens
|
|
627
|
+
state[:callbacks_completed] = false
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
def self.transfer_device_callback_keepalive(state, new_owner)
|
|
632
|
+
state[:mutex].synchronize do
|
|
633
|
+
return [] if state[:callbacks_completed]
|
|
634
|
+
|
|
635
|
+
previous_owner = state[:callback_owner]
|
|
636
|
+
tokens = Array(state[:callback_tokens])
|
|
637
|
+
transferred = tokens.select do |token|
|
|
638
|
+
CallbackKeepalive.transfer(previous_owner, new_owner, token)
|
|
639
|
+
end
|
|
640
|
+
state[:callback_owner] = new_owner
|
|
641
|
+
state[:callback_tokens] = transferred
|
|
642
|
+
transferred
|
|
643
|
+
end
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
def self.release_device_callback_keepalive(state)
|
|
647
|
+
owner, tokens = state[:mutex].synchronize do
|
|
648
|
+
return if state[:callbacks_completed]
|
|
649
|
+
|
|
650
|
+
state[:callbacks_completed] = true
|
|
651
|
+
retained_owner = state.delete(:callback_owner)
|
|
652
|
+
retained_tokens = Array(state.delete(:callback_tokens))
|
|
653
|
+
state.delete(:callback_lifetime)
|
|
654
|
+
[retained_owner, retained_tokens]
|
|
655
|
+
end
|
|
656
|
+
tokens.each { |token| CallbackKeepalive.release(owner, token) } if owner
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def self.release_abandoned_device(device, callback_state)
|
|
660
|
+
Native.wgpuDeviceRelease(device)
|
|
661
|
+
ensure
|
|
662
|
+
release_device_callback_keepalive(callback_state)
|
|
663
|
+
end
|
|
664
|
+
|
|
441
665
|
def self.dispatch_device_callback(state, key, *args)
|
|
442
666
|
handler = state[:mutex].synchronize { state[key] }
|
|
443
667
|
handler ? handler.call(*args) : yield
|
|
@@ -453,12 +677,95 @@ module WGPU
|
|
|
453
677
|
|
|
454
678
|
private_class_method :normalize_required_features, :normalize_feature_name,
|
|
455
679
|
:build_required_limits, :canonical_limit_key, :build_uncaptured_error_callback,
|
|
456
|
-
:build_device_lost_callback, :
|
|
680
|
+
:build_device_lost_callback, :retain_device_callback_lifetime,
|
|
681
|
+
:configure_device_callback_keepalive,
|
|
682
|
+
:transfer_device_callback_keepalive, :release_device_callback_keepalive,
|
|
683
|
+
:release_abandoned_device, :dispatch_device_callback, :string_from_callback
|
|
457
684
|
|
|
458
685
|
private
|
|
459
686
|
|
|
460
|
-
def
|
|
461
|
-
@
|
|
687
|
+
def begin_pop_error_scope
|
|
688
|
+
unless @error_scope_owner == Thread.current && @error_scope_depth.to_i.positive?
|
|
689
|
+
raise DeviceError, "No error scope belongs to the current thread"
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
error_holder = { done: false, status: nil, type: nil, message: nil }
|
|
693
|
+
|
|
694
|
+
callback_lifetime_release = device_callback_lifetime_lease
|
|
695
|
+
callback_token = nil
|
|
696
|
+
callback = FFI::Function.new(
|
|
697
|
+
:void, [:uint32, :uint32, Native::StringView.by_value, :pointer, :pointer]
|
|
698
|
+
) do |status, error_type, message, _userdata1, _userdata2|
|
|
699
|
+
begin
|
|
700
|
+
error_holder[:status] = Native::PopErrorScopeStatus[status]
|
|
701
|
+
error_holder[:type] = Native::ErrorType[error_type]
|
|
702
|
+
if message[:data] && !message[:data].null? && message[:length] > 0
|
|
703
|
+
error_holder[:message] = message[:data].read_string(message[:length])
|
|
704
|
+
end
|
|
705
|
+
error_holder[:done] = true
|
|
706
|
+
ensure
|
|
707
|
+
CallbackKeepalive.release(self, callback_token)
|
|
708
|
+
callback_lifetime_release.call
|
|
709
|
+
end
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
callback_info = Native::PopErrorScopeCallbackInfo.new
|
|
713
|
+
callback_info[:next_in_chain] = nil
|
|
714
|
+
callback_info[:mode] = AsyncWaiter.callback_mode(instance: @adapter&.instance)
|
|
715
|
+
callback_info[:callback] = callback
|
|
716
|
+
callback_info[:userdata1] = nil
|
|
717
|
+
callback_info[:userdata2] = nil
|
|
718
|
+
|
|
719
|
+
callback_token = CallbackKeepalive.retain(self, callback)
|
|
720
|
+
@error_scope_depth -= 1
|
|
721
|
+
@error_scope_owner = nil if @error_scope_depth.zero?
|
|
722
|
+
future =
|
|
723
|
+
begin
|
|
724
|
+
Native.wgpuDevicePopErrorScope(@handle, callback_info)
|
|
725
|
+
rescue StandardError
|
|
726
|
+
CallbackKeepalive.release(self, callback_token)
|
|
727
|
+
callback_lifetime_release.call
|
|
728
|
+
raise
|
|
729
|
+
ensure
|
|
730
|
+
error_scope_monitor.exit
|
|
731
|
+
end
|
|
732
|
+
[error_holder, future]
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
def wait_for_error_scope(error_holder, future, timeout:)
|
|
736
|
+
AsyncWaiter.wait(
|
|
737
|
+
status_holder: error_holder,
|
|
738
|
+
instance: @adapter&.instance,
|
|
739
|
+
device: self,
|
|
740
|
+
future: future,
|
|
741
|
+
timeout: timeout
|
|
742
|
+
)
|
|
743
|
+
error_holder
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
def error_scope_monitor
|
|
747
|
+
@error_scope_monitor ||= Monitor.new
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
# The reentrant scope lock spans push, the operation, and native pop.
|
|
751
|
+
def capture_error_scope(filter = :validation)
|
|
752
|
+
push_error_scope(filter)
|
|
753
|
+
begin
|
|
754
|
+
yield
|
|
755
|
+
ensure
|
|
756
|
+
active_error = $!
|
|
757
|
+
begin
|
|
758
|
+
error = pop_error_scope
|
|
759
|
+
rescue StandardError
|
|
760
|
+
raise unless active_error
|
|
761
|
+
end
|
|
762
|
+
end
|
|
763
|
+
error
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
def adopt_device_callback_keepalive
|
|
767
|
+
@device_callback_tokens =
|
|
768
|
+
self.class.send(:transfer_device_callback_keepalive, @device_callback_state, self)
|
|
462
769
|
end
|
|
463
770
|
|
|
464
771
|
def set_device_callback(name, handler)
|