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
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WGPU
|
|
4
|
+
class << self
|
|
5
|
+
attr_accessor :debug_leaks
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
self.debug_leaks = ENV["WGPU_DEBUG_LEAKS"] == "1"
|
|
9
|
+
|
|
10
|
+
module LeakTracker
|
|
11
|
+
@resources = {}
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
# Registers a live native resource when leak debugging is enabled.
|
|
17
|
+
#
|
|
18
|
+
# @param resource [NativeResource] resource to track
|
|
19
|
+
# @return [void]
|
|
20
|
+
def register(resource)
|
|
21
|
+
return unless WGPU.debug_leaks
|
|
22
|
+
|
|
23
|
+
object_id = resource.object_id
|
|
24
|
+
description = describe(resource)
|
|
25
|
+
@mutex.synchronize { @resources[object_id] = description }
|
|
26
|
+
ObjectSpace.define_finalizer(resource, finalizer(object_id))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Removes a resource from leak tracking.
|
|
30
|
+
#
|
|
31
|
+
# @param resource [NativeResource] resource that was released
|
|
32
|
+
# @return [void]
|
|
33
|
+
def unregister(resource)
|
|
34
|
+
@mutex.synchronize { @resources.delete(resource.object_id) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Warns about and clears every tracked resource.
|
|
38
|
+
#
|
|
39
|
+
# @return [void]
|
|
40
|
+
def warn_remaining
|
|
41
|
+
resources = @mutex.synchronize do
|
|
42
|
+
remaining = @resources.values
|
|
43
|
+
@resources.clear
|
|
44
|
+
remaining
|
|
45
|
+
end
|
|
46
|
+
resources.each { |description| warn "WGPU resource leaked: #{description}" }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def describe(resource)
|
|
50
|
+
label = resource.label
|
|
51
|
+
label_text = label ? " label=#{label.inspect}" : ""
|
|
52
|
+
"#{resource.class}#{label_text}"
|
|
53
|
+
end
|
|
54
|
+
private_class_method :describe
|
|
55
|
+
|
|
56
|
+
def finalizer(object_id)
|
|
57
|
+
proc do
|
|
58
|
+
description = @mutex.synchronize { @resources.delete(object_id) }
|
|
59
|
+
warn "WGPU resource leaked: #{description}" if description
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
private_class_method :finalizer
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
at_exit { LeakTracker.warn_remaining if WGPU.debug_leaks }
|
|
66
|
+
|
|
67
|
+
class DeviceCallbackLifetime
|
|
68
|
+
# Creates a shared lifetime that runs cleanup after its final owner releases.
|
|
69
|
+
# @yield cleanup invoked exactly once
|
|
70
|
+
def initialize(&cleanup)
|
|
71
|
+
@cleanup = cleanup
|
|
72
|
+
@references = 0
|
|
73
|
+
@completed = false
|
|
74
|
+
@mutex = Mutex.new
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Adds an owning native wrapper.
|
|
78
|
+
# @return [void]
|
|
79
|
+
def retain
|
|
80
|
+
@mutex.synchronize do
|
|
81
|
+
raise ResourceError, "device callback lifetime is already complete" if @completed
|
|
82
|
+
|
|
83
|
+
@references += 1
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Removes an owning native wrapper and cleans up after the final owner.
|
|
88
|
+
# @return [void]
|
|
89
|
+
def release
|
|
90
|
+
cleanup = @mutex.synchronize do
|
|
91
|
+
return if @completed
|
|
92
|
+
|
|
93
|
+
@references -= 1
|
|
94
|
+
raise ResourceError, "device callback lifetime reference underflow" if @references.negative?
|
|
95
|
+
next unless @references.zero?
|
|
96
|
+
|
|
97
|
+
@completed = true
|
|
98
|
+
@cleanup
|
|
99
|
+
end
|
|
100
|
+
cleanup&.call
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
private_constant :DeviceCallbackLifetime
|
|
104
|
+
|
|
105
|
+
module CallbackKeepalive
|
|
106
|
+
INITIALIZATION_MUTEX = Mutex.new
|
|
107
|
+
RETAINED_MUTEX = Mutex.new
|
|
108
|
+
RETAINED_CALLBACKS = {}
|
|
109
|
+
|
|
110
|
+
module_function
|
|
111
|
+
|
|
112
|
+
# Retains an FFI callback for as long as an operation needs it.
|
|
113
|
+
#
|
|
114
|
+
# @param owner [Object] object that owns the callback registry
|
|
115
|
+
# @param callback [FFI::Function] callback to retain
|
|
116
|
+
# @return [Object] opaque token used by {.release}
|
|
117
|
+
def retain(owner, callback)
|
|
118
|
+
mutex, callbacks = storage_for(owner)
|
|
119
|
+
token = Object.new
|
|
120
|
+
mutex.synchronize { callbacks[token] = callback }
|
|
121
|
+
RETAINED_MUTEX.synchronize { RETAINED_CALLBACKS[token] = callback }
|
|
122
|
+
token
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Releases a retained callback token.
|
|
126
|
+
#
|
|
127
|
+
# @param owner [Object] object that owns the callback registry
|
|
128
|
+
# @param token [Object, nil] token returned by {.retain}
|
|
129
|
+
# @return [void]
|
|
130
|
+
def release(owner, token)
|
|
131
|
+
return unless token
|
|
132
|
+
|
|
133
|
+
mutex, callbacks = storage_for(owner)
|
|
134
|
+
mutex.synchronize { callbacks.delete(token) }
|
|
135
|
+
RETAINED_MUTEX.synchronize { RETAINED_CALLBACKS.delete(token) }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Moves a retained callback token to another owner without unrooting it.
|
|
139
|
+
#
|
|
140
|
+
# @param from [Object] current registry owner
|
|
141
|
+
# @param to [Object] new registry owner
|
|
142
|
+
# @param token [Object] token returned by {.retain}
|
|
143
|
+
# @return [Boolean] whether the live token was transferred
|
|
144
|
+
def transfer(from, to, token)
|
|
145
|
+
return false unless token
|
|
146
|
+
|
|
147
|
+
callback = RETAINED_MUTEX.synchronize { RETAINED_CALLBACKS[token] }
|
|
148
|
+
return false unless callback
|
|
149
|
+
|
|
150
|
+
from_mutex, from_callbacks = storage_for(from)
|
|
151
|
+
to_mutex, to_callbacks = storage_for(to)
|
|
152
|
+
from_mutex.synchronize { from_callbacks.delete(token) }
|
|
153
|
+
to_mutex.synchronize { to_callbacks[token] = callback }
|
|
154
|
+
true
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Returns the number of callbacks retained for an owner.
|
|
158
|
+
#
|
|
159
|
+
# @param owner [Object] object that owns the callback registry
|
|
160
|
+
# @return [Integer] retained callback count
|
|
161
|
+
def count(owner)
|
|
162
|
+
mutex, callbacks = storage_for(owner)
|
|
163
|
+
mutex.synchronize { callbacks.length }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def storage_for(owner)
|
|
167
|
+
INITIALIZATION_MUTEX.synchronize do
|
|
168
|
+
mutex = owner.instance_variable_get(:@wgpu_callback_keepalive_mutex)
|
|
169
|
+
callbacks = owner.instance_variable_get(:@wgpu_callback_keepalive)
|
|
170
|
+
unless mutex && callbacks
|
|
171
|
+
mutex = Mutex.new
|
|
172
|
+
callbacks = {}
|
|
173
|
+
owner.instance_variable_set(:@wgpu_callback_keepalive_mutex, mutex)
|
|
174
|
+
owner.instance_variable_set(:@wgpu_callback_keepalive, callbacks)
|
|
175
|
+
end
|
|
176
|
+
[mutex, callbacks]
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
private_class_method :storage_for
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
module NativeResource
|
|
183
|
+
GUARDED_METHOD_EXEMPTIONS = [:initialize, :release, :released?, :handle, :label, :inspect].freeze
|
|
184
|
+
|
|
185
|
+
module ClassMethods
|
|
186
|
+
private
|
|
187
|
+
|
|
188
|
+
def adopt_native_handle(handle, label: nil)
|
|
189
|
+
resource = allocate
|
|
190
|
+
resource.send(:initialize_native_resource, handle: handle, label: label)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
module Lifecycle
|
|
195
|
+
UNSET = Object.new.freeze
|
|
196
|
+
private_constant :UNSET
|
|
197
|
+
|
|
198
|
+
# Initializes a wrapper and registers its native-resource lifecycle.
|
|
199
|
+
#
|
|
200
|
+
# @param args [Array] positional arguments forwarded to the resource
|
|
201
|
+
# @param kwargs [Hash] keyword arguments forwarded to the resource
|
|
202
|
+
# @yield block forwarded to the resource initializer
|
|
203
|
+
# @return [NativeResource] initialized resource
|
|
204
|
+
def initialize(*args, **kwargs, &block)
|
|
205
|
+
super
|
|
206
|
+
initialize_native_resource(label: kwargs.fetch(:label, UNSET))
|
|
207
|
+
attach_device_callback_lifetime_from_parent
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Releases a resource once and unregisters it from leak tracking.
|
|
211
|
+
#
|
|
212
|
+
# @return [void]
|
|
213
|
+
def release(...)
|
|
214
|
+
return if released?
|
|
215
|
+
|
|
216
|
+
result = super
|
|
217
|
+
release_device_callback_lifetime
|
|
218
|
+
@released = true
|
|
219
|
+
LeakTracker.unregister(self)
|
|
220
|
+
result
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
private
|
|
224
|
+
|
|
225
|
+
def initialize_native_resource(handle: UNSET, label: UNSET)
|
|
226
|
+
@handle = handle unless handle.equal?(UNSET)
|
|
227
|
+
@label = label unless label.equal?(UNSET)
|
|
228
|
+
@released = false
|
|
229
|
+
LeakTracker.register(self)
|
|
230
|
+
self
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def self.included(base)
|
|
235
|
+
guard = Module.new
|
|
236
|
+
base.public_instance_methods(false).each do |method_name|
|
|
237
|
+
next if GUARDED_METHOD_EXEMPTIONS.include?(method_name)
|
|
238
|
+
|
|
239
|
+
guard.define_method(method_name) do |*args, **kwargs, &block|
|
|
240
|
+
ensure_not_released!
|
|
241
|
+
super(*args, **kwargs, &block)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
base.extend(ClassMethods)
|
|
245
|
+
base.prepend(guard)
|
|
246
|
+
base.prepend(Lifecycle)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
attr_reader :label
|
|
250
|
+
|
|
251
|
+
# Reports whether the native handle has been released or is null.
|
|
252
|
+
# @return [Boolean]
|
|
253
|
+
def released?
|
|
254
|
+
return true if @released
|
|
255
|
+
return false unless instance_variable_defined?(:@handle)
|
|
256
|
+
return true if @handle.nil?
|
|
257
|
+
|
|
258
|
+
@handle.respond_to?(:null?) && @handle.null?
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Yields this wrapper and always releases it when the block exits.
|
|
262
|
+
#
|
|
263
|
+
# This is Ruby convenience API; WebGPU itself has no block-scoped resource
|
|
264
|
+
# primitive.
|
|
265
|
+
#
|
|
266
|
+
# @yieldparam resource [NativeResource] this resource
|
|
267
|
+
# @return the block result
|
|
268
|
+
def use
|
|
269
|
+
raise ArgumentError, "block is required" unless block_given?
|
|
270
|
+
|
|
271
|
+
ensure_not_released!
|
|
272
|
+
yield self
|
|
273
|
+
ensure
|
|
274
|
+
release if block_given? && !released?
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Returns a concise lifecycle-oriented representation.
|
|
278
|
+
#
|
|
279
|
+
# @return [String] class, optional label, and release state
|
|
280
|
+
def inspect
|
|
281
|
+
label_text = @label ? " label=#{@label.inspect}" : ""
|
|
282
|
+
"#<#{self.class}#{label_text} released=#{released?}>"
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
private
|
|
286
|
+
|
|
287
|
+
def attach_device_callback_lifetime(owner)
|
|
288
|
+
lifetime = owner&.instance_variable_get(:@device_callback_lifetime)
|
|
289
|
+
return self unless lifetime
|
|
290
|
+
|
|
291
|
+
lifetime.retain
|
|
292
|
+
@device_callback_lifetime = lifetime
|
|
293
|
+
@device_callback_lifetime_retained = true
|
|
294
|
+
self
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def attach_device_callback_lifetime_from_parent
|
|
298
|
+
return if @device_callback_lifetime_retained
|
|
299
|
+
|
|
300
|
+
if @device_callback_lifetime
|
|
301
|
+
@device_callback_lifetime.retain
|
|
302
|
+
@device_callback_lifetime_retained = true
|
|
303
|
+
return
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
owner = @device || @encoder || @texture
|
|
307
|
+
attach_device_callback_lifetime(owner)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def release_device_callback_lifetime
|
|
311
|
+
return unless @device_callback_lifetime_retained
|
|
312
|
+
|
|
313
|
+
@device_callback_lifetime_retained = false
|
|
314
|
+
@device_callback_lifetime.release
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def device_callback_lifetime_lease
|
|
318
|
+
lifetime = @device_callback_lifetime
|
|
319
|
+
return proc {} unless lifetime
|
|
320
|
+
|
|
321
|
+
lifetime.retain
|
|
322
|
+
released = false
|
|
323
|
+
mutex = Mutex.new
|
|
324
|
+
proc do
|
|
325
|
+
should_release = mutex.synchronize do
|
|
326
|
+
next false if released
|
|
327
|
+
|
|
328
|
+
released = true
|
|
329
|
+
true
|
|
330
|
+
end
|
|
331
|
+
lifetime.release if should_release
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def ensure_not_released!
|
|
336
|
+
return unless released?
|
|
337
|
+
|
|
338
|
+
label_text = @label ? " (#{@label})" : ""
|
|
339
|
+
raise ResourceError, "#{self.class}#{label_text} has been released"
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
end
|
|
@@ -4,6 +4,11 @@ module WGPU
|
|
|
4
4
|
class BindGroup
|
|
5
5
|
attr_reader :handle
|
|
6
6
|
|
|
7
|
+
# Creates bindings between shader slots and GPU resources.
|
|
8
|
+
# @param device [Device] owning device
|
|
9
|
+
# @param layout [BindGroupLayout] layout the entries must match
|
|
10
|
+
# @param entries [Array<Hash>] resource binding descriptors
|
|
11
|
+
# @raise [PipelineError] if native validation or creation fails
|
|
7
12
|
def initialize(device, label: nil, layout:, entries:)
|
|
8
13
|
@device = device
|
|
9
14
|
|
|
@@ -38,6 +43,10 @@ module WGPU
|
|
|
38
43
|
end
|
|
39
44
|
end
|
|
40
45
|
|
|
46
|
+
# Releases the native bind group handle.
|
|
47
|
+
#
|
|
48
|
+
# Calling this method more than once has no effect.
|
|
49
|
+
# @return [void]
|
|
41
50
|
def release
|
|
42
51
|
return if @handle.null?
|
|
43
52
|
Native.wgpuBindGroupRelease(@handle)
|
|
@@ -47,6 +56,18 @@ module WGPU
|
|
|
47
56
|
private
|
|
48
57
|
|
|
49
58
|
def create_entry(entry_hash)
|
|
59
|
+
DescriptorHelpers.validate_keys!(
|
|
60
|
+
entry_hash,
|
|
61
|
+
allowed: %i[binding buffer offset size sampler texture_view],
|
|
62
|
+
required: [:binding],
|
|
63
|
+
context: "bind group entry"
|
|
64
|
+
)
|
|
65
|
+
resources = %i[buffer sampler texture_view].select { |key| entry_hash[key] }
|
|
66
|
+
unless resources.one?
|
|
67
|
+
raise ArgumentError,
|
|
68
|
+
"bind group entry must define exactly one resource (:buffer, :sampler, or :texture_view)"
|
|
69
|
+
end
|
|
70
|
+
|
|
50
71
|
entry = Native::BindGroupEntry.new
|
|
51
72
|
entry[:next_in_chain] = nil
|
|
52
73
|
entry[:binding] = entry_hash[:binding]
|
|
@@ -4,39 +4,30 @@ module WGPU
|
|
|
4
4
|
class BindGroupLayout
|
|
5
5
|
attr_reader :handle
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
# Wraps a native bind group layout handle owned by the caller.
|
|
8
|
+
#
|
|
9
|
+
# @param handle [FFI::Pointer] native bind group layout handle
|
|
10
|
+
# @param device [Device, nil] device whose callbacks the layout may use
|
|
11
|
+
# @return [BindGroupLayout] adopted wrapper
|
|
12
|
+
def self.from_handle(handle, device: nil)
|
|
13
|
+
layout = adopt_native_handle(handle)
|
|
14
|
+
layout.instance_variable_set(:@device, device)
|
|
15
|
+
layout.send(:attach_device_callback_lifetime, device)
|
|
11
16
|
layout
|
|
12
17
|
end
|
|
13
18
|
|
|
19
|
+
# Creates a layout describing shader-visible resource bindings.
|
|
20
|
+
# @param device [Device] owning device
|
|
21
|
+
# @param entries [Array<Hash>] binding layout descriptors
|
|
22
|
+
# @raise [PipelineError] if native validation or creation fails
|
|
14
23
|
def initialize(device, label: nil, entries:)
|
|
15
24
|
@device = device
|
|
16
|
-
|
|
17
|
-
entries_array = entries.map { |e| create_entry(e) }
|
|
18
|
-
entries_ptr = FFI::MemoryPointer.new(Native::BindGroupLayoutEntry, entries_array.size)
|
|
19
|
-
entries_array.each_with_index do |entry, i|
|
|
20
|
-
offset = i * Native::BindGroupLayoutEntry.size
|
|
21
|
-
(entries_ptr + offset).put_bytes(0, entry.pointer.read_bytes(Native::BindGroupLayoutEntry.size))
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
desc = Native::BindGroupLayoutDescriptor.new
|
|
25
|
-
desc[:next_in_chain] = nil
|
|
26
|
-
if label
|
|
27
|
-
label_ptr = FFI::MemoryPointer.from_string(label)
|
|
28
|
-
desc[:label][:data] = label_ptr
|
|
29
|
-
desc[:label][:length] = label.bytesize
|
|
30
|
-
else
|
|
31
|
-
desc[:label][:data] = nil
|
|
32
|
-
desc[:label][:length] = 0
|
|
33
|
-
end
|
|
34
|
-
desc[:entry_count] = entries_array.size
|
|
35
|
-
desc[:entries] = entries_ptr
|
|
25
|
+
desc, @descriptor_keepalive = build_descriptor(label:, entries:)
|
|
36
26
|
|
|
37
27
|
device.push_error_scope(:validation)
|
|
38
28
|
@handle = Native.wgpuDeviceCreateBindGroupLayout(device.handle, desc)
|
|
39
29
|
error = device.pop_error_scope
|
|
30
|
+
@descriptor_keepalive = nil
|
|
40
31
|
|
|
41
32
|
if @handle.null? || (error[:type] && error[:type] != :no_error)
|
|
42
33
|
msg = error[:message] || "Failed to create bind group layout"
|
|
@@ -44,6 +35,10 @@ module WGPU
|
|
|
44
35
|
end
|
|
45
36
|
end
|
|
46
37
|
|
|
38
|
+
# Releases the native bind group layout handle.
|
|
39
|
+
#
|
|
40
|
+
# Calling this method more than once has no effect.
|
|
41
|
+
# @return [void]
|
|
47
42
|
def release
|
|
48
43
|
return if @handle.null?
|
|
49
44
|
Native.wgpuBindGroupLayoutRelease(@handle)
|
|
@@ -52,7 +47,39 @@ module WGPU
|
|
|
52
47
|
|
|
53
48
|
private
|
|
54
49
|
|
|
50
|
+
def build_descriptor(label:, entries:)
|
|
51
|
+
keepalive = []
|
|
52
|
+
entries_array = entries.map { |entry| create_entry(entry) }
|
|
53
|
+
entries_ptr = FFI::MemoryPointer.new(Native::BindGroupLayoutEntry, entries_array.size)
|
|
54
|
+
entries_array.each_with_index do |entry, index|
|
|
55
|
+
offset = index * Native::BindGroupLayoutEntry.size
|
|
56
|
+
(entries_ptr + offset).put_bytes(0, entry.pointer.read_bytes(Native::BindGroupLayoutEntry.size))
|
|
57
|
+
end
|
|
58
|
+
keepalive.concat(entries_array)
|
|
59
|
+
keepalive << entries_ptr
|
|
60
|
+
|
|
61
|
+
desc = Native::BindGroupLayoutDescriptor.new
|
|
62
|
+
desc[:next_in_chain] = nil
|
|
63
|
+
DescriptorHelpers.set_label(desc, label, keepalive:)
|
|
64
|
+
desc[:entry_count] = entries_array.size
|
|
65
|
+
desc[:entries] = entries_ptr
|
|
66
|
+
[desc, keepalive]
|
|
67
|
+
end
|
|
68
|
+
|
|
55
69
|
def create_entry(entry_hash)
|
|
70
|
+
DescriptorHelpers.validate_keys!(
|
|
71
|
+
entry_hash,
|
|
72
|
+
allowed: %i[binding visibility buffer sampler texture storage_texture],
|
|
73
|
+
required: %i[binding visibility],
|
|
74
|
+
context: "bind group layout entry"
|
|
75
|
+
)
|
|
76
|
+
variants = %i[buffer sampler texture storage_texture].select { |key| entry_hash[key] }
|
|
77
|
+
unless variants.one?
|
|
78
|
+
raise ArgumentError,
|
|
79
|
+
"bind group layout entry must define exactly one resource variant " \
|
|
80
|
+
"(:buffer, :sampler, :texture, or :storage_texture)"
|
|
81
|
+
end
|
|
82
|
+
|
|
56
83
|
entry = Native::BindGroupLayoutEntry.new
|
|
57
84
|
entry[:next_in_chain] = nil
|
|
58
85
|
entry[:binding] = entry_hash[:binding]
|
|
@@ -78,44 +105,84 @@ module WGPU
|
|
|
78
105
|
|
|
79
106
|
if entry_hash[:buffer]
|
|
80
107
|
buffer_info = entry_hash[:buffer]
|
|
81
|
-
|
|
108
|
+
DescriptorHelpers.validate_keys!(
|
|
109
|
+
buffer_info,
|
|
110
|
+
allowed: %i[type has_dynamic_offset min_binding_size],
|
|
111
|
+
context: "buffer binding layout"
|
|
112
|
+
)
|
|
113
|
+
entry[:buffer][:type] = Native::EnumHelper.coerce(
|
|
114
|
+
Native::BufferBindingType,
|
|
115
|
+
buffer_info[:type] || :storage,
|
|
116
|
+
name: "buffer binding type"
|
|
117
|
+
)
|
|
82
118
|
entry[:buffer][:has_dynamic_offset] = buffer_info[:has_dynamic_offset] ? 1 : 0
|
|
83
119
|
entry[:buffer][:min_binding_size] = buffer_info[:min_binding_size] || 0
|
|
84
120
|
end
|
|
85
121
|
|
|
86
122
|
if entry_hash[:sampler]
|
|
87
123
|
sampler_info = entry_hash[:sampler]
|
|
88
|
-
|
|
124
|
+
DescriptorHelpers.validate_keys!(
|
|
125
|
+
sampler_info,
|
|
126
|
+
allowed: [:type],
|
|
127
|
+
context: "sampler binding layout"
|
|
128
|
+
)
|
|
129
|
+
entry[:sampler][:type] = Native::EnumHelper.coerce(
|
|
130
|
+
Native::SamplerBindingType,
|
|
131
|
+
sampler_info[:type] || :filtering,
|
|
132
|
+
name: "sampler binding type"
|
|
133
|
+
)
|
|
89
134
|
end
|
|
90
135
|
|
|
91
136
|
if entry_hash[:texture]
|
|
92
137
|
texture_info = entry_hash[:texture]
|
|
93
|
-
|
|
94
|
-
|
|
138
|
+
DescriptorHelpers.validate_keys!(
|
|
139
|
+
texture_info,
|
|
140
|
+
allowed: %i[sample_type view_dimension multisampled],
|
|
141
|
+
context: "texture binding layout"
|
|
142
|
+
)
|
|
143
|
+
entry[:texture][:sample_type] = Native::EnumHelper.coerce(
|
|
144
|
+
Native::TextureSampleType,
|
|
145
|
+
texture_info[:sample_type] || :float,
|
|
146
|
+
name: "texture sample type"
|
|
147
|
+
)
|
|
148
|
+
entry[:texture][:view_dimension] = Native::EnumHelper.coerce(
|
|
149
|
+
Native::TextureViewDimension,
|
|
150
|
+
texture_info[:view_dimension] || :d2,
|
|
151
|
+
name: "texture view dimension"
|
|
152
|
+
)
|
|
95
153
|
entry[:texture][:multisampled] = texture_info[:multisampled] ? 1 : 0
|
|
96
154
|
end
|
|
97
155
|
|
|
98
156
|
if entry_hash[:storage_texture]
|
|
99
157
|
st_info = entry_hash[:storage_texture]
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
158
|
+
DescriptorHelpers.validate_keys!(
|
|
159
|
+
st_info,
|
|
160
|
+
allowed: %i[access format view_dimension],
|
|
161
|
+
required: [:format],
|
|
162
|
+
context: "storage texture binding layout"
|
|
163
|
+
)
|
|
164
|
+
entry[:storage_texture][:access] = Native::EnumHelper.coerce(
|
|
165
|
+
Native::StorageTextureAccess,
|
|
166
|
+
st_info[:access] || :write_only,
|
|
167
|
+
name: "storage texture access"
|
|
168
|
+
)
|
|
169
|
+
entry[:storage_texture][:format] = Native::EnumHelper.coerce(
|
|
170
|
+
Native::TextureFormat,
|
|
171
|
+
st_info.fetch(:format),
|
|
172
|
+
name: "storage texture format"
|
|
173
|
+
)
|
|
174
|
+
entry[:storage_texture][:view_dimension] = Native::EnumHelper.coerce(
|
|
175
|
+
Native::TextureViewDimension,
|
|
176
|
+
st_info[:view_dimension] || :d2,
|
|
177
|
+
name: "texture view dimension"
|
|
178
|
+
)
|
|
103
179
|
end
|
|
104
180
|
|
|
105
181
|
entry
|
|
106
182
|
end
|
|
107
183
|
|
|
108
184
|
def normalize_visibility(visibility)
|
|
109
|
-
|
|
110
|
-
when Integer
|
|
111
|
-
visibility
|
|
112
|
-
when Symbol
|
|
113
|
-
Native::ShaderStage[visibility]
|
|
114
|
-
when Array
|
|
115
|
-
visibility.reduce(0) { |acc, v| acc | Native::ShaderStage[v] }
|
|
116
|
-
else
|
|
117
|
-
raise ArgumentError, "Invalid visibility: #{visibility}"
|
|
118
|
-
end
|
|
185
|
+
Native::EnumHelper.coerce_flags(Native::ShaderStage, visibility, name: "shader visibility")
|
|
119
186
|
end
|
|
120
187
|
end
|
|
121
188
|
end
|
|
@@ -4,34 +4,14 @@ module WGPU
|
|
|
4
4
|
class ComputePipeline
|
|
5
5
|
attr_reader :handle
|
|
6
6
|
|
|
7
|
+
# Creates a compute pipeline.
|
|
8
|
+
# @param device [Device] owning device
|
|
9
|
+
# @param layout [PipelineLayout, :auto, nil] pipeline layout
|
|
10
|
+
# @param compute [Hash] programmable compute stage descriptor
|
|
11
|
+
# @raise [PipelineError] if native validation or creation fails
|
|
7
12
|
def initialize(device, label: nil, layout:, compute:)
|
|
8
13
|
@device = device
|
|
9
|
-
@pointers =
|
|
10
|
-
|
|
11
|
-
entry_point = compute[:entry_point] || "main"
|
|
12
|
-
entry_point_ptr = FFI::MemoryPointer.from_string(entry_point)
|
|
13
|
-
@pointers << entry_point_ptr
|
|
14
|
-
|
|
15
|
-
desc = Native::ComputePipelineDescriptor.new
|
|
16
|
-
desc[:next_in_chain] = nil
|
|
17
|
-
if label
|
|
18
|
-
label_ptr = FFI::MemoryPointer.from_string(label)
|
|
19
|
-
@pointers << label_ptr
|
|
20
|
-
desc[:label][:data] = label_ptr
|
|
21
|
-
desc[:label][:length] = label.bytesize
|
|
22
|
-
else
|
|
23
|
-
desc[:label][:data] = nil
|
|
24
|
-
desc[:label][:length] = 0
|
|
25
|
-
end
|
|
26
|
-
desc[:layout] = normalize_layout(layout)
|
|
27
|
-
|
|
28
|
-
desc[:compute][:next_in_chain] = nil
|
|
29
|
-
desc[:compute][:module] = compute.fetch(:module).handle
|
|
30
|
-
desc[:compute][:entry_point][:data] = entry_point_ptr
|
|
31
|
-
desc[:compute][:entry_point][:length] = entry_point.bytesize
|
|
32
|
-
desc[:compute][:constant_count] = 0
|
|
33
|
-
desc[:compute][:constants] = nil
|
|
34
|
-
setup_constants(desc[:compute], compute[:constants])
|
|
14
|
+
desc, @pointers = build_descriptor(label:, layout:, compute:)
|
|
35
15
|
|
|
36
16
|
device.push_error_scope(:validation)
|
|
37
17
|
@handle = Native.wgpuDeviceCreateComputePipeline(device.handle, desc)
|
|
@@ -43,12 +23,20 @@ module WGPU
|
|
|
43
23
|
end
|
|
44
24
|
end
|
|
45
25
|
|
|
26
|
+
# Returns the bind group layout inferred or assigned at an index.
|
|
27
|
+
# @param index [Integer] bind group index
|
|
28
|
+
# @return [BindGroupLayout]
|
|
29
|
+
# @raise [PipelineError] if no native layout is returned
|
|
46
30
|
def get_bind_group_layout(index)
|
|
47
31
|
handle = Native.wgpuComputePipelineGetBindGroupLayout(@handle, index)
|
|
48
32
|
raise PipelineError, "Failed to get bind group layout at index #{index}" if handle.null?
|
|
49
|
-
BindGroupLayout.from_handle(handle)
|
|
33
|
+
BindGroupLayout.from_handle(handle, device: @device)
|
|
50
34
|
end
|
|
51
35
|
|
|
36
|
+
# Releases the native compute pipeline handle.
|
|
37
|
+
#
|
|
38
|
+
# Calling this method more than once has no effect.
|
|
39
|
+
# @return [void]
|
|
52
40
|
def release
|
|
53
41
|
return if @handle.null?
|
|
54
42
|
Native.wgpuComputePipelineRelease(@handle)
|
|
@@ -57,32 +45,34 @@ module WGPU
|
|
|
57
45
|
|
|
58
46
|
private
|
|
59
47
|
|
|
48
|
+
def build_descriptor(label:, layout:, compute:)
|
|
49
|
+
DescriptorHelpers.validate_keys!(
|
|
50
|
+
compute,
|
|
51
|
+
allowed: %i[module entry_point constants],
|
|
52
|
+
required: [:module],
|
|
53
|
+
context: "compute pipeline stage"
|
|
54
|
+
)
|
|
55
|
+
@pointers = []
|
|
56
|
+
|
|
57
|
+
desc = Native::ComputePipelineDescriptor.new
|
|
58
|
+
desc[:next_in_chain] = nil
|
|
59
|
+
DescriptorHelpers.set_label(desc, label, keepalive: @pointers)
|
|
60
|
+
desc[:layout] = normalize_layout(layout)
|
|
61
|
+
desc[:compute][:next_in_chain] = nil
|
|
62
|
+
desc[:compute][:module] = compute.fetch(:module).handle
|
|
63
|
+
DescriptorHelpers.set_nullable_string_view(
|
|
64
|
+
desc[:compute][:entry_point],
|
|
65
|
+
compute[:entry_point],
|
|
66
|
+
keepalive: @pointers
|
|
67
|
+
)
|
|
68
|
+
DescriptorHelpers.set_constants(desc[:compute], compute[:constants], keepalive: @pointers)
|
|
69
|
+
|
|
70
|
+
[desc, @pointers]
|
|
71
|
+
end
|
|
72
|
+
|
|
60
73
|
def normalize_layout(layout)
|
|
61
74
|
return nil if layout.nil? || layout == :auto || layout == "auto"
|
|
62
75
|
layout.handle
|
|
63
76
|
end
|
|
64
|
-
|
|
65
|
-
def setup_constants(stage_desc, constants)
|
|
66
|
-
return if constants.nil? || constants.empty?
|
|
67
|
-
|
|
68
|
-
constants_ptr = FFI::MemoryPointer.new(Native::ConstantEntry, constants.size)
|
|
69
|
-
@pointers << constants_ptr
|
|
70
|
-
|
|
71
|
-
constants.each_with_index do |(key, value), i|
|
|
72
|
-
entry_ptr = constants_ptr + (i * Native::ConstantEntry.size)
|
|
73
|
-
entry = Native::ConstantEntry.new(entry_ptr)
|
|
74
|
-
entry[:next_in_chain] = nil
|
|
75
|
-
|
|
76
|
-
key_str = key.to_s
|
|
77
|
-
key_ptr = FFI::MemoryPointer.from_string(key_str)
|
|
78
|
-
@pointers << key_ptr
|
|
79
|
-
entry[:key][:data] = key_ptr
|
|
80
|
-
entry[:key][:length] = key_str.bytesize
|
|
81
|
-
entry[:value] = value.to_f
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
stage_desc[:constant_count] = constants.size
|
|
85
|
-
stage_desc[:constants] = constants_ptr
|
|
86
|
-
end
|
|
87
77
|
end
|
|
88
78
|
end
|