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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -0
- data/README.md +18 -2
- data/docs/README.md +2 -2
- data/docs/api_coverage.md +23 -15
- data/docs/async.md +10 -0
- data/docs/command_encoding.md +25 -0
- data/docs/getting_started_compute.md +1 -0
- data/docs/pipeline_descriptors.md +7 -2
- data/docs/releasing.md +11 -1
- data/docs/resource_lifetime.md +16 -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 +58 -1
- data/lib/wgpu/commands/compute_pass.rb +43 -0
- data/lib/wgpu/commands/render_bundle.rb +9 -1
- data/lib/wgpu/commands/render_bundle_encoder.rb +55 -1
- data/lib/wgpu/commands/render_pass.rb +102 -0
- data/lib/wgpu/core/adapter.rb +91 -12
- data/lib/wgpu/core/async_waiter.rb +15 -0
- data/lib/wgpu/core/canvas_context.rb +32 -0
- data/lib/wgpu/core/device.rb +296 -46
- data/lib/wgpu/core/instance.rb +20 -0
- data/lib/wgpu/core/queue.rb +139 -24
- data/lib/wgpu/core/surface.rb +49 -1
- data/lib/wgpu/data_types.rb +16 -0
- data/lib/wgpu/descriptor_helpers.rb +65 -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 +11 -0
- data/lib/wgpu/native/installer.rb +48 -17
- data/lib/wgpu/native/loader.rb +22 -0
- data/lib/wgpu/native/structs.rb +18 -1
- data/lib/wgpu/native_resource.rb +174 -3
- data/lib/wgpu/pipeline/bind_group.rb +9 -0
- data/lib/wgpu/pipeline/bind_group_layout.rb +17 -4
- data/lib/wgpu/pipeline/compute_pipeline.rb +20 -32
- data/lib/wgpu/pipeline/pipeline_layout.rb +8 -0
- data/lib/wgpu/pipeline/render_pipeline.rb +27 -42
- data/lib/wgpu/pipeline/shader_module.rb +54 -29
- data/lib/wgpu/resources/buffer.rb +258 -31
- data/lib/wgpu/resources/query_set.rb +12 -0
- data/lib/wgpu/resources/sampler.rb +7 -0
- data/lib/wgpu/resources/texture.rb +45 -6
- data/lib/wgpu/resources/texture_view.rb +24 -4
- 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 +84 -5
- metadata +3 -1
data/lib/wgpu/core/device.rb
CHANGED
|
@@ -6,24 +6,67 @@ module WGPU
|
|
|
6
6
|
|
|
7
7
|
LIMIT_FIELDS = Native::Limits.members.freeze
|
|
8
8
|
|
|
9
|
+
# Requests a logical device and waits for the native callback.
|
|
10
|
+
#
|
|
11
|
+
# @param adapter [Adapter] adapter that will create the device
|
|
12
|
+
# @param label [String, nil] optional debugging label
|
|
13
|
+
# @param required_features [Array<Symbol, Integer>] features the device must enable
|
|
14
|
+
# @param required_limits [Hash, nil] minimum required limits
|
|
15
|
+
# @param timeout [Numeric, nil] maximum wait time in seconds
|
|
16
|
+
# @return [Device] requested device
|
|
17
|
+
# @raise [DeviceError] if device creation fails
|
|
18
|
+
# @raise [TimeoutError] if the request exceeds +timeout+
|
|
19
|
+
# @raise [ArgumentError] if +timeout+ is used without an instance-backed adapter
|
|
9
20
|
def self.request(adapter, label: nil, required_features: [], required_limits: nil, timeout: nil)
|
|
21
|
+
if timeout && !adapter.instance
|
|
22
|
+
raise ArgumentError,
|
|
23
|
+
"Device.request timeout requires an instance-backed adapter; " \
|
|
24
|
+
"pass instance: to Adapter.from_handle"
|
|
25
|
+
end
|
|
26
|
+
|
|
10
27
|
device_ptr = FFI::MemoryPointer.new(:pointer)
|
|
11
|
-
status_holder = {
|
|
28
|
+
status_holder = {
|
|
29
|
+
done: false,
|
|
30
|
+
value: nil,
|
|
31
|
+
message: nil,
|
|
32
|
+
abandoned: false,
|
|
33
|
+
cleanup_claimed: false,
|
|
34
|
+
mutex: Mutex.new
|
|
35
|
+
}
|
|
12
36
|
device_callback_state = {
|
|
13
37
|
mutex: Mutex.new,
|
|
14
38
|
uncaptured_error: nil,
|
|
15
39
|
device_lost: nil
|
|
16
40
|
}
|
|
17
41
|
|
|
42
|
+
callback_token = nil
|
|
18
43
|
callback = FFI::Function.new(
|
|
19
44
|
:void, [:uint32, :pointer, Native::StringView.by_value, :pointer, :pointer]
|
|
20
45
|
) do |status, device, message, _userdata1, _userdata2|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
status_holder[:
|
|
46
|
+
cleanup_abandoned = false
|
|
47
|
+
begin
|
|
48
|
+
status_holder[:mutex].synchronize do
|
|
49
|
+
status_holder[:value] = Native::RequestDeviceStatus[status]
|
|
50
|
+
if message[:data] && !message[:data].null? && message[:length] > 0
|
|
51
|
+
status_holder[:message] = message[:data].read_string(message[:length])
|
|
52
|
+
end
|
|
53
|
+
device_ptr.write_pointer(device)
|
|
54
|
+
status_holder[:done] = true
|
|
55
|
+
if status_holder[:abandoned] && !status_holder[:cleanup_claimed]
|
|
56
|
+
status_holder[:cleanup_claimed] = true
|
|
57
|
+
cleanup_abandoned = true
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
if cleanup_abandoned
|
|
61
|
+
if status_holder[:value] == :success && device && !device.null?
|
|
62
|
+
release_abandoned_device(device, device_callback_state)
|
|
63
|
+
else
|
|
64
|
+
release_device_callback_keepalive(device_callback_state)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
ensure
|
|
68
|
+
CallbackKeepalive.release(adapter, callback_token)
|
|
24
69
|
end
|
|
25
|
-
device_ptr.write_pointer(device)
|
|
26
|
-
status_holder[:done] = true
|
|
27
70
|
end
|
|
28
71
|
|
|
29
72
|
queue_desc = Native::QueueDescriptor.new
|
|
@@ -81,35 +124,89 @@ module WGPU
|
|
|
81
124
|
callback_info[:userdata1] = nil
|
|
82
125
|
callback_info[:userdata2] = nil
|
|
83
126
|
|
|
127
|
+
device_lost_token = CallbackKeepalive.retain(adapter, device_lost_callback)
|
|
128
|
+
uncaptured_error_token = CallbackKeepalive.retain(adapter, uncaptured_error_callback)
|
|
129
|
+
configure_device_callback_keepalive(
|
|
130
|
+
device_callback_state,
|
|
131
|
+
owner: adapter,
|
|
132
|
+
tokens: [device_lost_token, uncaptured_error_token]
|
|
133
|
+
)
|
|
84
134
|
callback_token = CallbackKeepalive.retain(adapter, callback)
|
|
135
|
+
future =
|
|
136
|
+
begin
|
|
137
|
+
Native.wgpuAdapterRequestDevice(adapter.handle, desc, callback_info)
|
|
138
|
+
rescue StandardError
|
|
139
|
+
CallbackKeepalive.release(adapter, callback_token)
|
|
140
|
+
release_device_callback_keepalive(device_callback_state)
|
|
141
|
+
raise
|
|
142
|
+
end
|
|
143
|
+
|
|
85
144
|
begin
|
|
86
|
-
future = Native.wgpuAdapterRequestDevice(adapter.handle, desc, callback_info)
|
|
87
145
|
AsyncWaiter.wait(
|
|
88
146
|
status_holder: status_holder,
|
|
89
147
|
instance: adapter.instance,
|
|
90
148
|
future: future,
|
|
91
149
|
timeout: timeout
|
|
92
150
|
)
|
|
93
|
-
|
|
94
|
-
|
|
151
|
+
rescue TimeoutError
|
|
152
|
+
abandoned_device = nil
|
|
153
|
+
cleanup_abandoned = status_holder[:mutex].synchronize do
|
|
154
|
+
status_holder[:abandoned] = true
|
|
155
|
+
next false unless status_holder[:done] && !status_holder[:cleanup_claimed]
|
|
156
|
+
|
|
157
|
+
status_holder[:cleanup_claimed] = true
|
|
158
|
+
abandoned_device = device_ptr.read_pointer
|
|
159
|
+
true
|
|
160
|
+
end
|
|
161
|
+
if cleanup_abandoned
|
|
162
|
+
if status_holder[:value] == :success && abandoned_device && !abandoned_device.null?
|
|
163
|
+
release_abandoned_device(abandoned_device, device_callback_state)
|
|
164
|
+
else
|
|
165
|
+
release_device_callback_keepalive(device_callback_state)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
raise
|
|
95
169
|
end
|
|
96
170
|
|
|
97
171
|
handle = device_ptr.read_pointer
|
|
98
172
|
if handle.null? || status_holder[:value] != :success
|
|
173
|
+
release_device_callback_keepalive(device_callback_state)
|
|
99
174
|
msg = status_holder[:message] || "Unknown error"
|
|
100
175
|
raise DeviceError, "Failed to request device: #{msg}"
|
|
101
176
|
end
|
|
102
177
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
178
|
+
begin
|
|
179
|
+
callback_lifetime = DeviceCallbackLifetime.new do
|
|
180
|
+
release_device_callback_keepalive(device_callback_state)
|
|
181
|
+
end
|
|
182
|
+
device_callback_state[:mutex].synchronize do
|
|
183
|
+
device_callback_state[:callback_lifetime] = callback_lifetime
|
|
184
|
+
end
|
|
185
|
+
device = new(
|
|
186
|
+
handle,
|
|
187
|
+
adapter: adapter,
|
|
188
|
+
label: label,
|
|
189
|
+
callback_state: device_callback_state,
|
|
190
|
+
callback_lifetime: callback_lifetime
|
|
191
|
+
)
|
|
192
|
+
device.send(:adopt_device_callback_keepalive)
|
|
193
|
+
device
|
|
194
|
+
rescue StandardError
|
|
195
|
+
device ? device.release : release_abandoned_device(handle, device_callback_state)
|
|
196
|
+
raise
|
|
197
|
+
end
|
|
107
198
|
end
|
|
108
199
|
|
|
109
|
-
|
|
200
|
+
# Wraps a native device and obtains its default queue.
|
|
201
|
+
# @param handle [FFI::Pointer] native device handle
|
|
202
|
+
# @param adapter [Adapter, nil] adapter that created the device
|
|
203
|
+
# @param label [String, nil] optional debug label
|
|
204
|
+
# @param callback_lifetime [DeviceCallbackLifetime, nil] shared native callback lifetime
|
|
205
|
+
def initialize(handle, adapter: nil, label: nil, callback_state: nil, callback_lifetime: nil)
|
|
110
206
|
@handle = handle
|
|
111
207
|
@adapter = adapter
|
|
112
208
|
@label = label
|
|
209
|
+
@device_callback_lifetime = callback_lifetime
|
|
113
210
|
@device_callback_state = callback_state || {
|
|
114
211
|
mutex: Mutex.new,
|
|
115
212
|
uncaptured_error: nil,
|
|
@@ -119,14 +216,20 @@ module WGPU
|
|
|
119
216
|
@queue = Queue.new(Native.wgpuDeviceGetQueue(@handle), device: self)
|
|
120
217
|
end
|
|
121
218
|
|
|
219
|
+
# Returns information about the adapter that created this device.
|
|
220
|
+
# @return [Hash, nil]
|
|
122
221
|
def adapter_info
|
|
123
222
|
@adapter&.info
|
|
124
223
|
end
|
|
125
224
|
|
|
225
|
+
# Creates a GPU buffer.
|
|
226
|
+
# @return [Buffer]
|
|
126
227
|
def create_buffer(label: nil, size:, usage:, mapped_at_creation: false)
|
|
127
228
|
Buffer.new(self, label: label, size: size, usage: usage, mapped_at_creation: mapped_at_creation)
|
|
128
229
|
end
|
|
129
230
|
|
|
231
|
+
# Creates a shader module from WGSL or SPIR-V source.
|
|
232
|
+
# @return [ShaderModule]
|
|
130
233
|
def create_shader_module(label: nil, code: nil, spirv: nil, compilation_hints: [], validate: false)
|
|
131
234
|
ShaderModule.new(
|
|
132
235
|
self,
|
|
@@ -138,32 +241,46 @@ module WGPU
|
|
|
138
241
|
)
|
|
139
242
|
end
|
|
140
243
|
|
|
244
|
+
# Creates a command encoder.
|
|
245
|
+
# @return [CommandEncoder]
|
|
141
246
|
def create_command_encoder(label: nil)
|
|
142
247
|
CommandEncoder.new(self, label: label)
|
|
143
248
|
end
|
|
144
249
|
|
|
250
|
+
# Creates a bind group layout.
|
|
251
|
+
# @return [BindGroupLayout]
|
|
145
252
|
def create_bind_group_layout(label: nil, entries:)
|
|
146
253
|
BindGroupLayout.new(self, label: label, entries: entries)
|
|
147
254
|
end
|
|
148
255
|
|
|
256
|
+
# Creates a bind group.
|
|
257
|
+
# @return [BindGroup]
|
|
149
258
|
def create_bind_group(label: nil, layout:, entries:)
|
|
150
259
|
BindGroup.new(self, label: label, layout: layout, entries: entries)
|
|
151
260
|
end
|
|
152
261
|
|
|
262
|
+
# Creates a pipeline layout.
|
|
263
|
+
# @return [PipelineLayout]
|
|
153
264
|
def create_pipeline_layout(label: nil, bind_group_layouts:)
|
|
154
265
|
PipelineLayout.new(self, label: label, bind_group_layouts: bind_group_layouts)
|
|
155
266
|
end
|
|
156
267
|
|
|
268
|
+
# Creates a compute pipeline.
|
|
269
|
+
# @return [ComputePipeline]
|
|
157
270
|
def create_compute_pipeline(label: nil, layout:, compute:)
|
|
158
271
|
ComputePipeline.new(self, label: label, layout: layout, compute: compute)
|
|
159
272
|
end
|
|
160
273
|
|
|
274
|
+
# Creates a compute pipeline on a background task.
|
|
275
|
+
# @return [AsyncTask] task yielding a {ComputePipeline}
|
|
161
276
|
def create_compute_pipeline_async(label: nil, layout:, compute:)
|
|
162
277
|
AsyncTask.new do
|
|
163
278
|
create_compute_pipeline(label: label, layout: layout, compute: compute)
|
|
164
279
|
end
|
|
165
280
|
end
|
|
166
281
|
|
|
282
|
+
# Creates a render pipeline.
|
|
283
|
+
# @return [RenderPipeline]
|
|
167
284
|
def create_render_pipeline(label: nil, layout:, vertex:, primitive: {}, depth_stencil: nil, multisample: {}, fragment: nil)
|
|
168
285
|
RenderPipeline.new(self,
|
|
169
286
|
label: label,
|
|
@@ -176,6 +293,8 @@ module WGPU
|
|
|
176
293
|
)
|
|
177
294
|
end
|
|
178
295
|
|
|
296
|
+
# Creates a render pipeline on a background task.
|
|
297
|
+
# @return [AsyncTask] task yielding a {RenderPipeline}
|
|
179
298
|
def create_render_pipeline_async(label: nil, layout:, vertex:, primitive: {}, depth_stencil: nil, multisample: {}, fragment: nil)
|
|
180
299
|
AsyncTask.new do
|
|
181
300
|
create_render_pipeline(
|
|
@@ -190,6 +309,8 @@ module WGPU
|
|
|
190
309
|
end
|
|
191
310
|
end
|
|
192
311
|
|
|
312
|
+
# Creates a texture.
|
|
313
|
+
# @return [Texture]
|
|
193
314
|
def create_texture(label: nil, size:, format:, usage:, dimension: :d2, mip_level_count: 1, sample_count: 1, view_formats: [])
|
|
194
315
|
Texture.new(self,
|
|
195
316
|
label: label,
|
|
@@ -203,6 +324,8 @@ module WGPU
|
|
|
203
324
|
)
|
|
204
325
|
end
|
|
205
326
|
|
|
327
|
+
# Creates a texture sampler.
|
|
328
|
+
# @return [Sampler]
|
|
206
329
|
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
330
|
Sampler.new(self,
|
|
208
331
|
label: label,
|
|
@@ -219,6 +342,11 @@ module WGPU
|
|
|
219
342
|
)
|
|
220
343
|
end
|
|
221
344
|
|
|
345
|
+
# Creates a mapped buffer initialized from typed Ruby data.
|
|
346
|
+
# @param data [Array, String, FFI::Pointer] source data
|
|
347
|
+
# @param usage [Symbol, Array<Symbol>, Integer] buffer usage flags
|
|
348
|
+
# @param type [Symbol] source element type
|
|
349
|
+
# @return [Buffer]
|
|
222
350
|
def create_buffer_with_data(label: nil, data:, usage:, type: :f32)
|
|
223
351
|
data_ptr, byte_size = DataTypes.to_pointer(data, type:)
|
|
224
352
|
DataTypes.validate_alignment!(byte_size, 4, name: "buffer data size")
|
|
@@ -233,10 +361,14 @@ module WGPU
|
|
|
233
361
|
buffer
|
|
234
362
|
end
|
|
235
363
|
|
|
364
|
+
# Creates a GPU query set.
|
|
365
|
+
# @return [QuerySet]
|
|
236
366
|
def create_query_set(label: nil, type:, count:)
|
|
237
367
|
QuerySet.new(self, label: label, type: type, count: count)
|
|
238
368
|
end
|
|
239
369
|
|
|
370
|
+
# Creates an encoder for reusable render commands.
|
|
371
|
+
# @return [RenderBundleEncoder]
|
|
240
372
|
def create_render_bundle_encoder(color_formats:, depth_stencil_format: nil, sample_count: 1,
|
|
241
373
|
depth_read_only: false, stencil_read_only: false, label: nil)
|
|
242
374
|
RenderBundleEncoder.new(self,
|
|
@@ -249,6 +381,8 @@ module WGPU
|
|
|
249
381
|
)
|
|
250
382
|
end
|
|
251
383
|
|
|
384
|
+
# Lists optional features enabled on the device.
|
|
385
|
+
# @return [Array<Symbol>]
|
|
252
386
|
def features
|
|
253
387
|
supported = Native::SupportedFeatures.new
|
|
254
388
|
Native.wgpuDeviceGetFeatures(@handle, supported)
|
|
@@ -262,10 +396,15 @@ module WGPU
|
|
|
262
396
|
result
|
|
263
397
|
end
|
|
264
398
|
|
|
399
|
+
# Reports whether a feature is enabled on the device.
|
|
400
|
+
# @param feature [Symbol] feature name
|
|
401
|
+
# @return [Boolean]
|
|
265
402
|
def has_feature?(feature)
|
|
266
403
|
features.include?(feature)
|
|
267
404
|
end
|
|
268
405
|
|
|
406
|
+
# Returns resource limits supported by the device.
|
|
407
|
+
# @return [Hash{Symbol => Integer}]
|
|
269
408
|
def limits
|
|
270
409
|
supported = Native::SupportedLimits.new
|
|
271
410
|
supported[:next_in_chain] = nil
|
|
@@ -273,6 +412,9 @@ module WGPU
|
|
|
273
412
|
limits_to_hash(supported[:limits])
|
|
274
413
|
end
|
|
275
414
|
|
|
415
|
+
# Processes device work, optionally waiting for queue progress.
|
|
416
|
+
# @param wait [Boolean] whether to wait for submitted work
|
|
417
|
+
# @return [Integer] native poll status
|
|
276
418
|
def poll(wait: false)
|
|
277
419
|
if Native.device_poll_available?
|
|
278
420
|
Native.wgpuDevicePoll(@handle, wait ? 1 : 0, nil)
|
|
@@ -282,22 +424,35 @@ module WGPU
|
|
|
282
424
|
end
|
|
283
425
|
end
|
|
284
426
|
|
|
427
|
+
# Pushes a scope that captures matching GPU errors.
|
|
428
|
+
# @param filter [Symbol, Integer] error filter
|
|
429
|
+
# @return [void]
|
|
285
430
|
def push_error_scope(filter = :validation)
|
|
286
431
|
filter_value = Native::EnumHelper.coerce(Native::ErrorFilter, filter, name: "error filter")
|
|
287
432
|
Native.wgpuDevicePushErrorScope(@handle, filter_value)
|
|
288
433
|
end
|
|
289
434
|
|
|
435
|
+
# Pops the latest error scope and waits for its result.
|
|
436
|
+
# @param timeout [Numeric, nil] maximum wait time in seconds
|
|
437
|
+
# @return [Hash] native status, error type, and message
|
|
290
438
|
def pop_error_scope(timeout: nil)
|
|
291
439
|
error_holder = { done: false, status: nil, type: nil, message: nil }
|
|
292
440
|
|
|
441
|
+
callback_lifetime_release = device_callback_lifetime_lease
|
|
442
|
+
callback_token = nil
|
|
293
443
|
callback = FFI::Function.new(
|
|
294
444
|
:void, [:uint32, :uint32, Native::StringView.by_value, :pointer, :pointer]
|
|
295
445
|
) do |status, error_type, message, _userdata1, _userdata2|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
446
|
+
begin
|
|
447
|
+
error_holder[:status] = Native::PopErrorScopeStatus[status]
|
|
448
|
+
error_holder[:type] = Native::ErrorType[error_type]
|
|
449
|
+
if message[:data] && !message[:data].null? && message[:length] > 0
|
|
450
|
+
error_holder[:message] = message[:data].read_string(message[:length])
|
|
451
|
+
end
|
|
452
|
+
error_holder[:done] = true
|
|
453
|
+
ensure
|
|
454
|
+
CallbackKeepalive.release(self, callback_token)
|
|
455
|
+
callback_lifetime_release.call
|
|
301
456
|
end
|
|
302
457
|
end
|
|
303
458
|
|
|
@@ -309,30 +464,44 @@ module WGPU
|
|
|
309
464
|
callback_info[:userdata2] = nil
|
|
310
465
|
|
|
311
466
|
callback_token = CallbackKeepalive.retain(self, callback)
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
467
|
+
future =
|
|
468
|
+
begin
|
|
469
|
+
Native.wgpuDevicePopErrorScope(@handle, callback_info)
|
|
470
|
+
rescue StandardError
|
|
471
|
+
CallbackKeepalive.release(self, callback_token)
|
|
472
|
+
callback_lifetime_release.call
|
|
473
|
+
raise
|
|
474
|
+
end
|
|
475
|
+
AsyncWaiter.wait(
|
|
476
|
+
status_holder: error_holder,
|
|
477
|
+
instance: @adapter&.instance,
|
|
478
|
+
device: self,
|
|
479
|
+
future: future,
|
|
480
|
+
timeout: timeout
|
|
481
|
+
)
|
|
324
482
|
|
|
325
483
|
error_holder
|
|
326
484
|
end
|
|
327
485
|
|
|
486
|
+
# Pops the latest error scope on a background thread.
|
|
487
|
+
#
|
|
488
|
+
# @param timeout [Numeric, nil] maximum wait time in seconds
|
|
489
|
+
# @return [AsyncTask] task whose value is the error hash
|
|
328
490
|
def pop_error_scope_async(timeout: nil)
|
|
329
491
|
AsyncTask.new { pop_error_scope(timeout: timeout) }
|
|
330
492
|
end
|
|
331
493
|
|
|
494
|
+
# Pops the latest error scope as a typed error.
|
|
495
|
+
# @param timeout [Numeric, nil] maximum wait time in seconds
|
|
496
|
+
# @return [GPUError, nil]
|
|
332
497
|
def pop_error_scope_typed(timeout: nil)
|
|
333
498
|
GPUError.from_hash(pop_error_scope(timeout: timeout))
|
|
334
499
|
end
|
|
335
500
|
|
|
501
|
+
# Runs a block inside an error scope and raises captured GPU errors.
|
|
502
|
+
# @param filter [Symbol, Integer] error filter
|
|
503
|
+
# @yieldreturn [Object] block result
|
|
504
|
+
# @return [Object] block result
|
|
336
505
|
def with_error_scope(filter = :validation)
|
|
337
506
|
push_error_scope(filter)
|
|
338
507
|
result = yield
|
|
@@ -341,6 +510,10 @@ module WGPU
|
|
|
341
510
|
result
|
|
342
511
|
end
|
|
343
512
|
|
|
513
|
+
# Registers the handler for uncaptured GPU errors.
|
|
514
|
+
# @yieldparam error [GPUError] reported error
|
|
515
|
+
# @return [Device] this device
|
|
516
|
+
# @raise [ArgumentError] if no block is provided
|
|
344
517
|
def on_uncaptured_error(&handler)
|
|
345
518
|
raise ArgumentError, "on_uncaptured_error requires a block" unless handler
|
|
346
519
|
|
|
@@ -348,6 +521,11 @@ module WGPU
|
|
|
348
521
|
self
|
|
349
522
|
end
|
|
350
523
|
|
|
524
|
+
# Registers the handler invoked when the device is lost.
|
|
525
|
+
# @yieldparam reason [Symbol] native device-lost reason
|
|
526
|
+
# @yieldparam message [String] native diagnostic message
|
|
527
|
+
# @return [Device] this device
|
|
528
|
+
# @raise [ArgumentError] if no block is provided
|
|
351
529
|
def on_device_lost(&handler)
|
|
352
530
|
raise ArgumentError, "on_device_lost requires a block" unless handler
|
|
353
531
|
|
|
@@ -355,18 +533,26 @@ module WGPU
|
|
|
355
533
|
self
|
|
356
534
|
end
|
|
357
535
|
|
|
536
|
+
# Destroys device-owned native resources.
|
|
537
|
+
# @return [void]
|
|
358
538
|
def destroy
|
|
359
539
|
return if @handle.null?
|
|
360
540
|
Native.wgpuDeviceDestroy(@handle)
|
|
361
541
|
end
|
|
362
542
|
|
|
543
|
+
# Releases the default queue and native device handle.
|
|
544
|
+
#
|
|
545
|
+
# Device callbacks remain alive until all derived wrappers and pending
|
|
546
|
+
# callback operations have also completed their native releases.
|
|
547
|
+
#
|
|
548
|
+
# Calling this method more than once has no effect.
|
|
549
|
+
# @return [void]
|
|
363
550
|
def release
|
|
364
551
|
@queue&.release
|
|
365
552
|
return if @handle.null?
|
|
366
553
|
Native.wgpuDeviceRelease(@handle)
|
|
367
554
|
@handle = FFI::Pointer::NULL
|
|
368
|
-
|
|
369
|
-
@device_callback_tokens&.clear
|
|
555
|
+
@device_callback_tokens = nil
|
|
370
556
|
end
|
|
371
557
|
|
|
372
558
|
def self.normalize_required_features(required_features)
|
|
@@ -416,12 +602,17 @@ module WGPU
|
|
|
416
602
|
FFI::Function.new(
|
|
417
603
|
:void, [:pointer, :uint32, Native::StringView.by_value, :pointer, :pointer]
|
|
418
604
|
) do |_device, type, message, _userdata1, _userdata2|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
605
|
+
callback_lifetime = retain_device_callback_lifetime(state)
|
|
606
|
+
begin
|
|
607
|
+
error = GPUError.new(
|
|
608
|
+
type: Native::ErrorType[type] || :unknown,
|
|
609
|
+
message: string_from_callback(message)
|
|
610
|
+
)
|
|
611
|
+
dispatch_device_callback(state, :uncaptured_error, error) do
|
|
612
|
+
warn "Uncaptured GPU error (#{error.type}): #{error.message}"
|
|
613
|
+
end
|
|
614
|
+
ensure
|
|
615
|
+
callback_lifetime&.release
|
|
425
616
|
end
|
|
426
617
|
end
|
|
427
618
|
end
|
|
@@ -430,12 +621,67 @@ module WGPU
|
|
|
430
621
|
FFI::Function.new(
|
|
431
622
|
:void, [:pointer, :uint32, Native::StringView.by_value, :pointer, :pointer]
|
|
432
623
|
) do |_device, reason, message, _userdata1, _userdata2|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
624
|
+
callback_lifetime = retain_device_callback_lifetime(state)
|
|
625
|
+
begin
|
|
626
|
+
reason_name = Native::DeviceLostReason[reason] || :unknown
|
|
627
|
+
message_text = string_from_callback(message)
|
|
628
|
+
dispatch_device_callback(state, :device_lost, reason_name, message_text) do
|
|
629
|
+
warn "GPU device lost (#{reason_name}): #{message_text}" unless reason_name == :destroyed
|
|
630
|
+
end
|
|
631
|
+
ensure
|
|
632
|
+
callback_lifetime&.release
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
def self.retain_device_callback_lifetime(state)
|
|
638
|
+
state[:mutex].synchronize do
|
|
639
|
+
lifetime = state[:callback_lifetime]
|
|
640
|
+
lifetime&.retain
|
|
641
|
+
lifetime
|
|
642
|
+
end
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
def self.configure_device_callback_keepalive(state, owner:, tokens:)
|
|
646
|
+
state[:mutex].synchronize do
|
|
647
|
+
state[:callback_owner] = owner
|
|
648
|
+
state[:callback_tokens] = tokens
|
|
649
|
+
state[:callbacks_completed] = false
|
|
650
|
+
end
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
def self.transfer_device_callback_keepalive(state, new_owner)
|
|
654
|
+
state[:mutex].synchronize do
|
|
655
|
+
return [] if state[:callbacks_completed]
|
|
656
|
+
|
|
657
|
+
previous_owner = state[:callback_owner]
|
|
658
|
+
tokens = Array(state[:callback_tokens])
|
|
659
|
+
transferred = tokens.select do |token|
|
|
660
|
+
CallbackKeepalive.transfer(previous_owner, new_owner, token)
|
|
437
661
|
end
|
|
662
|
+
state[:callback_owner] = new_owner
|
|
663
|
+
state[:callback_tokens] = transferred
|
|
664
|
+
transferred
|
|
665
|
+
end
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
def self.release_device_callback_keepalive(state)
|
|
669
|
+
owner, tokens = state[:mutex].synchronize do
|
|
670
|
+
return if state[:callbacks_completed]
|
|
671
|
+
|
|
672
|
+
state[:callbacks_completed] = true
|
|
673
|
+
retained_owner = state.delete(:callback_owner)
|
|
674
|
+
retained_tokens = Array(state.delete(:callback_tokens))
|
|
675
|
+
state.delete(:callback_lifetime)
|
|
676
|
+
[retained_owner, retained_tokens]
|
|
438
677
|
end
|
|
678
|
+
tokens.each { |token| CallbackKeepalive.release(owner, token) } if owner
|
|
679
|
+
end
|
|
680
|
+
|
|
681
|
+
def self.release_abandoned_device(device, callback_state)
|
|
682
|
+
Native.wgpuDeviceRelease(device)
|
|
683
|
+
ensure
|
|
684
|
+
release_device_callback_keepalive(callback_state)
|
|
439
685
|
end
|
|
440
686
|
|
|
441
687
|
def self.dispatch_device_callback(state, key, *args)
|
|
@@ -453,12 +699,16 @@ module WGPU
|
|
|
453
699
|
|
|
454
700
|
private_class_method :normalize_required_features, :normalize_feature_name,
|
|
455
701
|
:build_required_limits, :canonical_limit_key, :build_uncaptured_error_callback,
|
|
456
|
-
:build_device_lost_callback, :
|
|
702
|
+
:build_device_lost_callback, :retain_device_callback_lifetime,
|
|
703
|
+
:configure_device_callback_keepalive,
|
|
704
|
+
:transfer_device_callback_keepalive, :release_device_callback_keepalive,
|
|
705
|
+
:release_abandoned_device, :dispatch_device_callback, :string_from_callback
|
|
457
706
|
|
|
458
707
|
private
|
|
459
708
|
|
|
460
|
-
def
|
|
461
|
-
@device_callback_tokens
|
|
709
|
+
def adopt_device_callback_keepalive
|
|
710
|
+
@device_callback_tokens =
|
|
711
|
+
self.class.send(:transfer_device_callback_keepalive, @device_callback_state, self)
|
|
462
712
|
end
|
|
463
713
|
|
|
464
714
|
def set_device_callback(name, handler)
|
data/lib/wgpu/core/instance.rb
CHANGED
|
@@ -4,6 +4,8 @@ module WGPU
|
|
|
4
4
|
class Instance
|
|
5
5
|
attr_reader :handle
|
|
6
6
|
|
|
7
|
+
# Creates a WebGPU instance.
|
|
8
|
+
# @raise [InitializationError] if native instance creation fails
|
|
7
9
|
def initialize
|
|
8
10
|
desc = Native::InstanceDescriptor.new
|
|
9
11
|
desc[:next_in_chain] = nil
|
|
@@ -15,6 +17,8 @@ module WGPU
|
|
|
15
17
|
raise InitializationError, "Failed to create WebGPU instance" if @handle.null?
|
|
16
18
|
end
|
|
17
19
|
|
|
20
|
+
# Requests an adapter matching the supplied preferences.
|
|
21
|
+
# @return [Adapter]
|
|
18
22
|
def request_adapter(power_preference: :high_performance, backend: nil, feature_level: :core,
|
|
19
23
|
force_fallback_adapter: false, compatible_surface: nil, timeout: nil)
|
|
20
24
|
Adapter.request(
|
|
@@ -28,6 +32,8 @@ module WGPU
|
|
|
28
32
|
)
|
|
29
33
|
end
|
|
30
34
|
|
|
35
|
+
# Requests an adapter on a background task.
|
|
36
|
+
# @return [AsyncTask] task yielding an {Adapter}
|
|
31
37
|
def request_adapter_async(power_preference: :high_performance, backend: nil, feature_level: :core,
|
|
32
38
|
force_fallback_adapter: false, compatible_surface: nil, timeout: nil)
|
|
33
39
|
AsyncTask.new do
|
|
@@ -42,6 +48,9 @@ module WGPU
|
|
|
42
48
|
end
|
|
43
49
|
end
|
|
44
50
|
|
|
51
|
+
# Lists adapters exposed by the instance.
|
|
52
|
+
# @param backends [Integer, nil] backend bit mask
|
|
53
|
+
# @return [Array<Adapter>]
|
|
45
54
|
def enumerate_adapters(backends: nil)
|
|
46
55
|
options = nil
|
|
47
56
|
if backends
|
|
@@ -61,20 +70,31 @@ module WGPU
|
|
|
61
70
|
end
|
|
62
71
|
end
|
|
63
72
|
|
|
73
|
+
# Enumerates adapters on a background task.
|
|
74
|
+
# @return [AsyncTask] task yielding adapter objects
|
|
64
75
|
def enumerate_adapters_async(backends: nil)
|
|
65
76
|
AsyncTask.new do
|
|
66
77
|
enumerate_adapters(backends: backends)
|
|
67
78
|
end
|
|
68
79
|
end
|
|
69
80
|
|
|
81
|
+
# Creates a canvas context from platform presentation information.
|
|
82
|
+
# @param present_info [Hash] platform surface information
|
|
83
|
+
# @return [CanvasContext]
|
|
70
84
|
def get_canvas_context(present_info)
|
|
71
85
|
CanvasContext.new(self, present_info)
|
|
72
86
|
end
|
|
73
87
|
|
|
88
|
+
# Processes pending instance callbacks and events.
|
|
89
|
+
# @return [void]
|
|
74
90
|
def process_events
|
|
75
91
|
Native.wgpuInstanceProcessEvents(@handle)
|
|
76
92
|
end
|
|
77
93
|
|
|
94
|
+
# Releases the native instance handle.
|
|
95
|
+
#
|
|
96
|
+
# Calling this method more than once has no effect.
|
|
97
|
+
# @return [void]
|
|
78
98
|
def release
|
|
79
99
|
return if @handle.null?
|
|
80
100
|
Native.wgpuInstanceRelease(@handle)
|