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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +88 -0
- data/README.md +44 -5
- data/docs/README.md +22 -0
- data/docs/api_coverage.md +140 -0
- data/docs/async.md +41 -0
- data/docs/bind_groups.md +25 -0
- data/docs/buffer_data.md +37 -0
- data/docs/command_encoding.md +49 -0
- data/docs/errors.md +40 -0
- data/docs/getting_started_compute.md +62 -0
- data/docs/getting_started_rendering.md +62 -0
- data/docs/installation.md +94 -0
- data/docs/pipeline_descriptors.md +43 -0
- data/docs/releasing.md +32 -0
- data/docs/resource_lifetime.md +108 -0
- data/docs/shaders.md +32 -0
- data/docs/texture_readback.md +46 -0
- data/docs/troubleshooting.md +53 -0
- data/docs/upgrading_wgpu_native.md +37 -0
- data/ext/wgpu/extconf.rb +10 -142
- data/lib/wgpu/async_task.rb +19 -0
- data/lib/wgpu/commands/command_buffer.rb +25 -1
- data/lib/wgpu/commands/command_encoder.rb +123 -9
- data/lib/wgpu/commands/compute_pass.rb +53 -0
- data/lib/wgpu/commands/render_bundle.rb +9 -1
- data/lib/wgpu/commands/render_bundle_encoder.rb +65 -4
- data/lib/wgpu/commands/render_pass.rb +136 -8
- data/lib/wgpu/core/adapter.rb +123 -19
- data/lib/wgpu/core/async_waiter.rb +47 -4
- data/lib/wgpu/core/canvas_context.rb +32 -2
- data/lib/wgpu/core/device.rb +399 -53
- data/lib/wgpu/core/instance.rb +28 -4
- data/lib/wgpu/core/queue.rb +197 -51
- data/lib/wgpu/core/surface.rb +64 -17
- data/lib/wgpu/data_types.rb +83 -0
- data/lib/wgpu/descriptor_helpers.rb +104 -0
- data/lib/wgpu/error.rb +70 -0
- data/lib/wgpu/logging.rb +63 -0
- data/lib/wgpu/native/abi_verifier.rb +143 -0
- data/lib/wgpu/native/callbacks.rb +10 -1
- data/lib/wgpu/native/capabilities.rb +32 -2
- data/lib/wgpu/native/distribution.rb +176 -0
- data/lib/wgpu/native/enum_helper.rb +80 -0
- data/lib/wgpu/native/enums.rb +68 -8
- data/lib/wgpu/native/fixtures/webgpu-v27.0.4.0-enums.h +848 -0
- data/lib/wgpu/native/functions.rb +21 -10
- data/lib/wgpu/native/installer.rb +223 -0
- data/lib/wgpu/native/loader.rb +61 -21
- data/lib/wgpu/native/structs.rb +18 -1
- data/lib/wgpu/native_resource.rb +342 -0
- data/lib/wgpu/pipeline/bind_group.rb +21 -0
- data/lib/wgpu/pipeline/bind_group_layout.rb +108 -41
- data/lib/wgpu/pipeline/compute_pipeline.rb +40 -50
- data/lib/wgpu/pipeline/pipeline_layout.rb +8 -0
- data/lib/wgpu/pipeline/render_pipeline.rb +207 -110
- data/lib/wgpu/pipeline/shader_module.rb +95 -28
- data/lib/wgpu/resources/buffer.rb +387 -85
- data/lib/wgpu/resources/query_set.rb +26 -12
- data/lib/wgpu/resources/sampler.rb +43 -20
- data/lib/wgpu/resources/texture.rb +89 -48
- data/lib/wgpu/resources/texture_view.rb +35 -7
- data/lib/wgpu/texture_format.rb +96 -0
- data/lib/wgpu/version.rb +1 -1
- data/lib/wgpu/window.rb +34 -1
- data/lib/wgpu.rb +32 -0
- data/sig/wgpu.rbs +460 -0
- metadata +33 -16
data/lib/wgpu/core/device.rb
CHANGED
|
@@ -6,19 +6,67 @@ module WGPU
|
|
|
6
6
|
|
|
7
7
|
LIMIT_FIELDS = Native::Limits.members.freeze
|
|
8
8
|
|
|
9
|
-
|
|
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
|
|
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
|
+
}
|
|
36
|
+
device_callback_state = {
|
|
37
|
+
mutex: Mutex.new,
|
|
38
|
+
uncaptured_error: nil,
|
|
39
|
+
device_lost: nil
|
|
40
|
+
}
|
|
12
41
|
|
|
42
|
+
callback_token = nil
|
|
13
43
|
callback = FFI::Function.new(
|
|
14
44
|
:void, [:uint32, :pointer, Native::StringView.by_value, :pointer, :pointer]
|
|
15
45
|
) do |status, device, message, _userdata1, _userdata2|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
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)
|
|
19
69
|
end
|
|
20
|
-
device_ptr.write_pointer(device)
|
|
21
|
-
status_holder[:done] = true
|
|
22
70
|
end
|
|
23
71
|
|
|
24
72
|
queue_desc = Native::QueueDescriptor.new
|
|
@@ -28,14 +76,16 @@ module WGPU
|
|
|
28
76
|
|
|
29
77
|
device_lost_info = Native::DeviceLostCallbackInfo.new
|
|
30
78
|
device_lost_info[:next_in_chain] = nil
|
|
31
|
-
device_lost_info[:mode] =
|
|
32
|
-
|
|
79
|
+
device_lost_info[:mode] = AsyncWaiter.callback_mode(instance: adapter.instance)
|
|
80
|
+
device_lost_callback = build_device_lost_callback(device_callback_state)
|
|
81
|
+
device_lost_info[:callback] = device_lost_callback
|
|
33
82
|
device_lost_info[:userdata1] = nil
|
|
34
83
|
device_lost_info[:userdata2] = nil
|
|
35
84
|
|
|
36
85
|
error_info = Native::UncapturedErrorCallbackInfo.new
|
|
37
86
|
error_info[:next_in_chain] = nil
|
|
38
|
-
|
|
87
|
+
uncaptured_error_callback = build_uncaptured_error_callback(device_callback_state)
|
|
88
|
+
error_info[:callback] = uncaptured_error_callback
|
|
39
89
|
error_info[:userdata1] = nil
|
|
40
90
|
error_info[:userdata2] = nil
|
|
41
91
|
|
|
@@ -74,62 +124,163 @@ module WGPU
|
|
|
74
124
|
callback_info[:userdata1] = nil
|
|
75
125
|
callback_info[:userdata2] = nil
|
|
76
126
|
|
|
77
|
-
|
|
78
|
-
|
|
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
|
+
)
|
|
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
|
+
|
|
144
|
+
begin
|
|
145
|
+
AsyncWaiter.wait(
|
|
146
|
+
status_holder: status_holder,
|
|
147
|
+
instance: adapter.instance,
|
|
148
|
+
future: future,
|
|
149
|
+
timeout: timeout
|
|
150
|
+
)
|
|
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
|
|
169
|
+
end
|
|
79
170
|
|
|
80
171
|
handle = device_ptr.read_pointer
|
|
81
172
|
if handle.null? || status_holder[:value] != :success
|
|
173
|
+
release_device_callback_keepalive(device_callback_state)
|
|
82
174
|
msg = status_holder[:message] || "Unknown error"
|
|
83
175
|
raise DeviceError, "Failed to request device: #{msg}"
|
|
84
176
|
end
|
|
85
177
|
|
|
86
|
-
|
|
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
|
|
87
198
|
end
|
|
88
199
|
|
|
89
|
-
|
|
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)
|
|
90
206
|
@handle = handle
|
|
91
207
|
@adapter = adapter
|
|
208
|
+
@label = label
|
|
209
|
+
@device_callback_lifetime = callback_lifetime
|
|
210
|
+
@device_callback_state = callback_state || {
|
|
211
|
+
mutex: Mutex.new,
|
|
212
|
+
uncaptured_error: nil,
|
|
213
|
+
device_lost: nil
|
|
214
|
+
}
|
|
215
|
+
@device_callback_tokens = []
|
|
92
216
|
@queue = Queue.new(Native.wgpuDeviceGetQueue(@handle), device: self)
|
|
93
217
|
end
|
|
94
218
|
|
|
219
|
+
# Returns information about the adapter that created this device.
|
|
220
|
+
# @return [Hash, nil]
|
|
95
221
|
def adapter_info
|
|
96
222
|
@adapter&.info
|
|
97
223
|
end
|
|
98
224
|
|
|
225
|
+
# Creates a GPU buffer.
|
|
226
|
+
# @return [Buffer]
|
|
99
227
|
def create_buffer(label: nil, size:, usage:, mapped_at_creation: false)
|
|
100
228
|
Buffer.new(self, label: label, size: size, usage: usage, mapped_at_creation: mapped_at_creation)
|
|
101
229
|
end
|
|
102
230
|
|
|
103
|
-
|
|
104
|
-
|
|
231
|
+
# Creates a shader module from WGSL or SPIR-V source.
|
|
232
|
+
# @return [ShaderModule]
|
|
233
|
+
def create_shader_module(label: nil, code: nil, spirv: nil, compilation_hints: [], validate: false)
|
|
234
|
+
ShaderModule.new(
|
|
235
|
+
self,
|
|
236
|
+
label: label,
|
|
237
|
+
code: code,
|
|
238
|
+
spirv: spirv,
|
|
239
|
+
compilation_hints: compilation_hints,
|
|
240
|
+
validate: validate
|
|
241
|
+
)
|
|
105
242
|
end
|
|
106
243
|
|
|
244
|
+
# Creates a command encoder.
|
|
245
|
+
# @return [CommandEncoder]
|
|
107
246
|
def create_command_encoder(label: nil)
|
|
108
247
|
CommandEncoder.new(self, label: label)
|
|
109
248
|
end
|
|
110
249
|
|
|
250
|
+
# Creates a bind group layout.
|
|
251
|
+
# @return [BindGroupLayout]
|
|
111
252
|
def create_bind_group_layout(label: nil, entries:)
|
|
112
253
|
BindGroupLayout.new(self, label: label, entries: entries)
|
|
113
254
|
end
|
|
114
255
|
|
|
256
|
+
# Creates a bind group.
|
|
257
|
+
# @return [BindGroup]
|
|
115
258
|
def create_bind_group(label: nil, layout:, entries:)
|
|
116
259
|
BindGroup.new(self, label: label, layout: layout, entries: entries)
|
|
117
260
|
end
|
|
118
261
|
|
|
262
|
+
# Creates a pipeline layout.
|
|
263
|
+
# @return [PipelineLayout]
|
|
119
264
|
def create_pipeline_layout(label: nil, bind_group_layouts:)
|
|
120
265
|
PipelineLayout.new(self, label: label, bind_group_layouts: bind_group_layouts)
|
|
121
266
|
end
|
|
122
267
|
|
|
268
|
+
# Creates a compute pipeline.
|
|
269
|
+
# @return [ComputePipeline]
|
|
123
270
|
def create_compute_pipeline(label: nil, layout:, compute:)
|
|
124
271
|
ComputePipeline.new(self, label: label, layout: layout, compute: compute)
|
|
125
272
|
end
|
|
126
273
|
|
|
274
|
+
# Creates a compute pipeline on a background task.
|
|
275
|
+
# @return [AsyncTask] task yielding a {ComputePipeline}
|
|
127
276
|
def create_compute_pipeline_async(label: nil, layout:, compute:)
|
|
128
277
|
AsyncTask.new do
|
|
129
278
|
create_compute_pipeline(label: label, layout: layout, compute: compute)
|
|
130
279
|
end
|
|
131
280
|
end
|
|
132
281
|
|
|
282
|
+
# Creates a render pipeline.
|
|
283
|
+
# @return [RenderPipeline]
|
|
133
284
|
def create_render_pipeline(label: nil, layout:, vertex:, primitive: {}, depth_stencil: nil, multisample: {}, fragment: nil)
|
|
134
285
|
RenderPipeline.new(self,
|
|
135
286
|
label: label,
|
|
@@ -142,6 +293,8 @@ module WGPU
|
|
|
142
293
|
)
|
|
143
294
|
end
|
|
144
295
|
|
|
296
|
+
# Creates a render pipeline on a background task.
|
|
297
|
+
# @return [AsyncTask] task yielding a {RenderPipeline}
|
|
145
298
|
def create_render_pipeline_async(label: nil, layout:, vertex:, primitive: {}, depth_stencil: nil, multisample: {}, fragment: nil)
|
|
146
299
|
AsyncTask.new do
|
|
147
300
|
create_render_pipeline(
|
|
@@ -156,6 +309,8 @@ module WGPU
|
|
|
156
309
|
end
|
|
157
310
|
end
|
|
158
311
|
|
|
312
|
+
# Creates a texture.
|
|
313
|
+
# @return [Texture]
|
|
159
314
|
def create_texture(label: nil, size:, format:, usage:, dimension: :d2, mip_level_count: 1, sample_count: 1, view_formats: [])
|
|
160
315
|
Texture.new(self,
|
|
161
316
|
label: label,
|
|
@@ -169,6 +324,8 @@ module WGPU
|
|
|
169
324
|
)
|
|
170
325
|
end
|
|
171
326
|
|
|
327
|
+
# Creates a texture sampler.
|
|
328
|
+
# @return [Sampler]
|
|
172
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)
|
|
173
330
|
Sampler.new(self,
|
|
174
331
|
label: label,
|
|
@@ -185,8 +342,14 @@ module WGPU
|
|
|
185
342
|
)
|
|
186
343
|
end
|
|
187
344
|
|
|
188
|
-
|
|
189
|
-
|
|
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]
|
|
350
|
+
def create_buffer_with_data(label: nil, data:, usage:, type: :f32)
|
|
351
|
+
data_ptr, byte_size = DataTypes.to_pointer(data, type:)
|
|
352
|
+
DataTypes.validate_alignment!(byte_size, 4, name: "buffer data size")
|
|
190
353
|
buffer = create_buffer(
|
|
191
354
|
label: label,
|
|
192
355
|
size: byte_size,
|
|
@@ -198,10 +361,14 @@ module WGPU
|
|
|
198
361
|
buffer
|
|
199
362
|
end
|
|
200
363
|
|
|
364
|
+
# Creates a GPU query set.
|
|
365
|
+
# @return [QuerySet]
|
|
201
366
|
def create_query_set(label: nil, type:, count:)
|
|
202
367
|
QuerySet.new(self, label: label, type: type, count: count)
|
|
203
368
|
end
|
|
204
369
|
|
|
370
|
+
# Creates an encoder for reusable render commands.
|
|
371
|
+
# @return [RenderBundleEncoder]
|
|
205
372
|
def create_render_bundle_encoder(color_formats:, depth_stencil_format: nil, sample_count: 1,
|
|
206
373
|
depth_read_only: false, stencil_read_only: false, label: nil)
|
|
207
374
|
RenderBundleEncoder.new(self,
|
|
@@ -214,6 +381,8 @@ module WGPU
|
|
|
214
381
|
)
|
|
215
382
|
end
|
|
216
383
|
|
|
384
|
+
# Lists optional features enabled on the device.
|
|
385
|
+
# @return [Array<Symbol>]
|
|
217
386
|
def features
|
|
218
387
|
supported = Native::SupportedFeatures.new
|
|
219
388
|
Native.wgpuDeviceGetFeatures(@handle, supported)
|
|
@@ -227,10 +396,15 @@ module WGPU
|
|
|
227
396
|
result
|
|
228
397
|
end
|
|
229
398
|
|
|
399
|
+
# Reports whether a feature is enabled on the device.
|
|
400
|
+
# @param feature [Symbol] feature name
|
|
401
|
+
# @return [Boolean]
|
|
230
402
|
def has_feature?(feature)
|
|
231
403
|
features.include?(feature)
|
|
232
404
|
end
|
|
233
405
|
|
|
406
|
+
# Returns resource limits supported by the device.
|
|
407
|
+
# @return [Hash{Symbol => Integer}]
|
|
234
408
|
def limits
|
|
235
409
|
supported = Native::SupportedLimits.new
|
|
236
410
|
supported[:next_in_chain] = nil
|
|
@@ -238,6 +412,9 @@ module WGPU
|
|
|
238
412
|
limits_to_hash(supported[:limits])
|
|
239
413
|
end
|
|
240
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
|
|
241
418
|
def poll(wait: false)
|
|
242
419
|
if Native.device_poll_available?
|
|
243
420
|
Native.wgpuDevicePoll(@handle, wait ? 1 : 0, nil)
|
|
@@ -247,21 +424,35 @@ module WGPU
|
|
|
247
424
|
end
|
|
248
425
|
end
|
|
249
426
|
|
|
427
|
+
# Pushes a scope that captures matching GPU errors.
|
|
428
|
+
# @param filter [Symbol, Integer] error filter
|
|
429
|
+
# @return [void]
|
|
250
430
|
def push_error_scope(filter = :validation)
|
|
251
|
-
Native.
|
|
431
|
+
filter_value = Native::EnumHelper.coerce(Native::ErrorFilter, filter, name: "error filter")
|
|
432
|
+
Native.wgpuDevicePushErrorScope(@handle, filter_value)
|
|
252
433
|
end
|
|
253
434
|
|
|
254
|
-
|
|
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
|
|
438
|
+
def pop_error_scope(timeout: nil)
|
|
255
439
|
error_holder = { done: false, status: nil, type: nil, message: nil }
|
|
256
440
|
|
|
441
|
+
callback_lifetime_release = device_callback_lifetime_lease
|
|
442
|
+
callback_token = nil
|
|
257
443
|
callback = FFI::Function.new(
|
|
258
444
|
:void, [:uint32, :uint32, Native::StringView.by_value, :pointer, :pointer]
|
|
259
445
|
) do |status, error_type, message, _userdata1, _userdata2|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
|
265
456
|
end
|
|
266
457
|
end
|
|
267
458
|
|
|
@@ -272,40 +463,98 @@ module WGPU
|
|
|
272
463
|
callback_info[:userdata1] = nil
|
|
273
464
|
callback_info[:userdata2] = nil
|
|
274
465
|
|
|
275
|
-
|
|
276
|
-
|
|
466
|
+
callback_token = CallbackKeepalive.retain(self, callback)
|
|
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
|
+
)
|
|
277
482
|
|
|
278
483
|
error_holder
|
|
279
484
|
end
|
|
280
485
|
|
|
281
|
-
|
|
282
|
-
|
|
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
|
|
490
|
+
def pop_error_scope_async(timeout: nil)
|
|
491
|
+
AsyncTask.new { pop_error_scope(timeout: timeout) }
|
|
283
492
|
end
|
|
284
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]
|
|
497
|
+
def pop_error_scope_typed(timeout: nil)
|
|
498
|
+
GPUError.from_hash(pop_error_scope(timeout: timeout))
|
|
499
|
+
end
|
|
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
|
|
285
505
|
def with_error_scope(filter = :validation)
|
|
286
506
|
push_error_scope(filter)
|
|
287
507
|
result = yield
|
|
288
|
-
error = pop_error_scope
|
|
289
|
-
|
|
290
|
-
raise Error, "GPU error (#{error[:type]}): #{error[:message]}"
|
|
291
|
-
end
|
|
508
|
+
error = GPUError.from_hash(pop_error_scope)
|
|
509
|
+
error&.raise!
|
|
292
510
|
result
|
|
293
511
|
end
|
|
294
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
|
|
517
|
+
def on_uncaptured_error(&handler)
|
|
518
|
+
raise ArgumentError, "on_uncaptured_error requires a block" unless handler
|
|
519
|
+
|
|
520
|
+
set_device_callback(:uncaptured_error, handler)
|
|
521
|
+
self
|
|
522
|
+
end
|
|
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
|
|
529
|
+
def on_device_lost(&handler)
|
|
530
|
+
raise ArgumentError, "on_device_lost requires a block" unless handler
|
|
531
|
+
|
|
532
|
+
set_device_callback(:device_lost, handler)
|
|
533
|
+
self
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Destroys device-owned native resources.
|
|
537
|
+
# @return [void]
|
|
295
538
|
def destroy
|
|
296
539
|
return if @handle.null?
|
|
297
540
|
Native.wgpuDeviceDestroy(@handle)
|
|
298
541
|
end
|
|
299
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]
|
|
300
550
|
def release
|
|
301
551
|
@queue&.release
|
|
302
552
|
return if @handle.null?
|
|
303
553
|
Native.wgpuDeviceRelease(@handle)
|
|
304
554
|
@handle = FFI::Pointer::NULL
|
|
555
|
+
@device_callback_tokens = nil
|
|
305
556
|
end
|
|
306
557
|
|
|
307
|
-
private
|
|
308
|
-
|
|
309
558
|
def self.normalize_required_features(required_features)
|
|
310
559
|
Array(required_features).map do |feature|
|
|
311
560
|
normalize_feature_name(feature)
|
|
@@ -316,10 +565,7 @@ module WGPU
|
|
|
316
565
|
return feature if feature.is_a?(Integer)
|
|
317
566
|
|
|
318
567
|
key = feature.to_s.strip.tr("-", "_").to_sym
|
|
319
|
-
|
|
320
|
-
raise ArgumentError, "Unknown feature name: #{feature}" if value.nil?
|
|
321
|
-
|
|
322
|
-
value
|
|
568
|
+
Native::EnumHelper.coerce(Native::FeatureName, key, name: "feature name")
|
|
323
569
|
end
|
|
324
570
|
|
|
325
571
|
def self.build_required_limits(adapter, required_limits)
|
|
@@ -352,21 +598,121 @@ module WGPU
|
|
|
352
598
|
raise ArgumentError, "Unknown limit key: #{name}"
|
|
353
599
|
end
|
|
354
600
|
|
|
355
|
-
def
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
601
|
+
def self.build_uncaptured_error_callback(state)
|
|
602
|
+
FFI::Function.new(
|
|
603
|
+
:void, [:pointer, :uint32, Native::StringView.by_value, :pointer, :pointer]
|
|
604
|
+
) do |_device, type, message, _userdata1, _userdata2|
|
|
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
|
|
616
|
+
end
|
|
617
|
+
end
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
def self.build_device_lost_callback(state)
|
|
621
|
+
FFI::Function.new(
|
|
622
|
+
:void, [:pointer, :uint32, Native::StringView.by_value, :pointer, :pointer]
|
|
623
|
+
) do |_device, reason, message, _userdata1, _userdata2|
|
|
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)
|
|
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]
|
|
369
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)
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
def self.dispatch_device_callback(state, key, *args)
|
|
688
|
+
handler = state[:mutex].synchronize { state[key] }
|
|
689
|
+
handler ? handler.call(*args) : yield
|
|
690
|
+
rescue StandardError => e
|
|
691
|
+
warn "WGPU #{key} handler failed: #{e.class}: #{e.message}"
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
def self.string_from_callback(message)
|
|
695
|
+
return "" if message[:data].nil? || message[:data].null? || message[:length].zero?
|
|
696
|
+
|
|
697
|
+
message[:data].read_string(message[:length])
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
private_class_method :normalize_required_features, :normalize_feature_name,
|
|
701
|
+
:build_required_limits, :canonical_limit_key, :build_uncaptured_error_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
|
|
706
|
+
|
|
707
|
+
private
|
|
708
|
+
|
|
709
|
+
def adopt_device_callback_keepalive
|
|
710
|
+
@device_callback_tokens =
|
|
711
|
+
self.class.send(:transfer_device_callback_keepalive, @device_callback_state, self)
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
def set_device_callback(name, handler)
|
|
715
|
+
@device_callback_state[:mutex].synchronize { @device_callback_state[name] = handler }
|
|
370
716
|
end
|
|
371
717
|
|
|
372
718
|
def limits_to_hash(limits)
|