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
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module WGPU
|
|
4
4
|
module Native
|
|
5
|
+
attach_function :wgpuGetVersion,
|
|
6
|
+
[], :uint32
|
|
7
|
+
|
|
5
8
|
attach_function :wgpuCreateInstance,
|
|
6
9
|
[InstanceDescriptor.by_ref], :pointer
|
|
7
10
|
|
|
@@ -68,9 +71,17 @@ module WGPU
|
|
|
68
71
|
attach_function :wgpuDeviceCreateRenderPipeline,
|
|
69
72
|
[:pointer, :pointer], :pointer
|
|
70
73
|
|
|
74
|
+
attach_function :wgpuDeviceCreateRenderPipelineAsync,
|
|
75
|
+
[:pointer, RenderPipelineDescriptor.by_ref, CreateRenderPipelineAsyncCallbackInfo.by_value],
|
|
76
|
+
Future.by_value
|
|
77
|
+
|
|
71
78
|
attach_function :wgpuDeviceCreateComputePipeline,
|
|
72
79
|
[:pointer, ComputePipelineDescriptor.by_ref], :pointer
|
|
73
80
|
|
|
81
|
+
attach_function :wgpuDeviceCreateComputePipelineAsync,
|
|
82
|
+
[:pointer, ComputePipelineDescriptor.by_ref, CreateComputePipelineAsyncCallbackInfo.by_value],
|
|
83
|
+
Future.by_value
|
|
84
|
+
|
|
74
85
|
attach_function :wgpuDeviceCreateCommandEncoder,
|
|
75
86
|
[:pointer, CommandEncoderDescriptor.by_ref], :pointer
|
|
76
87
|
|
|
@@ -281,6 +292,9 @@ module WGPU
|
|
|
281
292
|
attach_function :wgpuTextureViewRelease,
|
|
282
293
|
[:pointer], :void
|
|
283
294
|
|
|
295
|
+
attach_function :wgpuSupportedFeaturesFreeMembers,
|
|
296
|
+
[SupportedFeatures.by_value], :void
|
|
297
|
+
|
|
284
298
|
attach_function :wgpuAdapterInfoFreeMembers,
|
|
285
299
|
[AdapterInfo.by_value], :void
|
|
286
300
|
|
|
@@ -11,22 +11,50 @@ module WGPU
|
|
|
11
11
|
module Native
|
|
12
12
|
class InstallError < StandardError; end
|
|
13
13
|
|
|
14
|
+
# Downloads, verifies, and installs the pinned wgpu-native artifact.
|
|
14
15
|
class Installer
|
|
15
16
|
attr_reader :platform
|
|
16
17
|
|
|
18
|
+
# Creates an installer for a target Ruby platform.
|
|
19
|
+
#
|
|
20
|
+
# The final four dependencies are injectable to keep network, process,
|
|
21
|
+
# archive, and distribution branches testable without external effects.
|
|
22
|
+
#
|
|
23
|
+
# @param platform [String] target Ruby platform
|
|
24
|
+
# @param env [Hash] environment variables
|
|
25
|
+
# @param home [String] home directory used for cache resolution
|
|
26
|
+
# @param host_os [String] host operating-system identifier
|
|
27
|
+
# @param output [IO] progress output
|
|
28
|
+
# @param command_runner [#call, nil] process runner
|
|
29
|
+
# @param http_factory [#call, nil] factory receiving a URI
|
|
30
|
+
# @param zip_file_loader [#call, nil] factory returning Zip::File
|
|
31
|
+
# @param distribution [Module] artifact metadata provider
|
|
17
32
|
def initialize(platform: RUBY_PLATFORM, env: ENV, home: Dir.home,
|
|
18
|
-
host_os: RbConfig::CONFIG["host_os"], output: $stdout
|
|
33
|
+
host_os: RbConfig::CONFIG["host_os"], output: $stdout,
|
|
34
|
+
command_runner: nil, http_factory: nil, zip_file_loader: nil,
|
|
35
|
+
distribution: Distribution)
|
|
19
36
|
@platform = platform
|
|
20
37
|
@env = env
|
|
21
38
|
@home = home
|
|
22
39
|
@host_os = host_os
|
|
23
40
|
@output = output
|
|
24
|
-
|
|
25
|
-
|
|
41
|
+
@command_runner = command_runner || method(:system)
|
|
42
|
+
@http_factory = http_factory || ->(uri) { Net::HTTP.new(uri.host, uri.port) }
|
|
43
|
+
@zip_file_loader = zip_file_loader || lambda {
|
|
44
|
+
require "zip"
|
|
45
|
+
Zip::File
|
|
46
|
+
}
|
|
47
|
+
@distribution = distribution
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Installs or reuses the native library for {#platform}.
|
|
51
|
+
#
|
|
52
|
+
# @return [String] absolute path to the native library
|
|
53
|
+
# @raise [InstallError] if the artifact cannot be obtained or verified
|
|
26
54
|
def install
|
|
27
55
|
return custom_library_path if custom_library_path
|
|
28
56
|
|
|
29
|
-
artifact =
|
|
57
|
+
artifact = @distribution.artifact_for(platform)
|
|
30
58
|
cached_path = existing_library_path(artifact)
|
|
31
59
|
if cached_path
|
|
32
60
|
@output.puts "wgpu-native already cached at #{cached_path}"
|
|
@@ -38,8 +66,11 @@ module WGPU
|
|
|
38
66
|
raise InstallError, "#{error.message}\n\n#{recovery_instructions}"
|
|
39
67
|
end
|
|
40
68
|
|
|
69
|
+
# Returns the primary versioned cache directory.
|
|
70
|
+
#
|
|
71
|
+
# @return [String]
|
|
41
72
|
def clean_path
|
|
42
|
-
|
|
73
|
+
@distribution.primary_cache_dir(env: @env, home: @home, host_os: @host_os)
|
|
43
74
|
end
|
|
44
75
|
|
|
45
76
|
private
|
|
@@ -53,7 +84,7 @@ module WGPU
|
|
|
53
84
|
end
|
|
54
85
|
|
|
55
86
|
def existing_library_path(artifact)
|
|
56
|
-
|
|
87
|
+
@distribution.cache_directories(env: @env, home: @home, host_os: @host_os).each do |directory|
|
|
57
88
|
path = File.join(directory, "lib", artifact.fetch(:library))
|
|
58
89
|
return path if File.file?(path)
|
|
59
90
|
end
|
|
@@ -66,9 +97,9 @@ module WGPU
|
|
|
66
97
|
library_path = File.join(cache_dir, "lib", artifact.fetch(:library))
|
|
67
98
|
FileUtils.mkdir_p(cache_dir)
|
|
68
99
|
|
|
69
|
-
@output.puts "Downloading wgpu-native #{
|
|
70
|
-
@output.puts " URL: #{
|
|
71
|
-
download_file(
|
|
100
|
+
@output.puts "Downloading wgpu-native #{@distribution.const_get(:VERSION)} from GitHub..."
|
|
101
|
+
@output.puts " URL: #{@distribution.release_url(artifact)}"
|
|
102
|
+
download_file(@distribution.release_url(artifact), archive_path)
|
|
72
103
|
verify_checksum!(archive_path, artifact.fetch(:sha256))
|
|
73
104
|
|
|
74
105
|
@output.puts "Extracting to #{cache_dir}..."
|
|
@@ -97,18 +128,18 @@ module WGPU
|
|
|
97
128
|
end
|
|
98
129
|
|
|
99
130
|
def curl_available?
|
|
100
|
-
|
|
131
|
+
@command_runner.call("curl", "--version", out: File::NULL, err: File::NULL)
|
|
101
132
|
end
|
|
102
133
|
|
|
103
134
|
def download_with_curl(url, destination)
|
|
104
|
-
|
|
135
|
+
@command_runner.call("curl", "-fsSL", "-o", destination, url)
|
|
105
136
|
end
|
|
106
137
|
|
|
107
138
|
def download_with_ruby(url, destination, redirects = 5)
|
|
108
139
|
raise InstallError, "Too many redirects while downloading #{url}" if redirects.zero?
|
|
109
140
|
|
|
110
141
|
uri = URI.parse(url)
|
|
111
|
-
http =
|
|
142
|
+
http = @http_factory.call(uri)
|
|
112
143
|
http.use_ssl = uri.scheme == "https"
|
|
113
144
|
http.open_timeout = 10
|
|
114
145
|
http.read_timeout = 120
|
|
@@ -144,10 +175,10 @@ module WGPU
|
|
|
144
175
|
end
|
|
145
176
|
|
|
146
177
|
def extract_with_rubyzip(archive_path, destination)
|
|
147
|
-
|
|
178
|
+
zip_file_class = @zip_file_loader.call
|
|
148
179
|
destination_root = "#{File.expand_path(destination)}#{File::SEPARATOR}"
|
|
149
180
|
|
|
150
|
-
|
|
181
|
+
zip_file_class.open(archive_path) do |zip_file|
|
|
151
182
|
zip_file.each do |entry|
|
|
152
183
|
target = File.expand_path(entry.name, destination)
|
|
153
184
|
unless target.start_with?(destination_root)
|
|
@@ -155,7 +186,13 @@ module WGPU
|
|
|
155
186
|
end
|
|
156
187
|
|
|
157
188
|
FileUtils.mkdir_p(entry.directory? ? target : File.dirname(target))
|
|
158
|
-
|
|
189
|
+
next if entry.directory?
|
|
190
|
+
|
|
191
|
+
if entry.method(:extract).parameters.include?([:key, :destination_directory])
|
|
192
|
+
entry.extract(entry.name, destination_directory: destination) { true }
|
|
193
|
+
else
|
|
194
|
+
entry.extract(target) { true }
|
|
195
|
+
end
|
|
159
196
|
end
|
|
160
197
|
end
|
|
161
198
|
true
|
|
@@ -164,7 +201,7 @@ module WGPU
|
|
|
164
201
|
end
|
|
165
202
|
|
|
166
203
|
def extract_with_powershell(archive_path, destination)
|
|
167
|
-
|
|
204
|
+
@command_runner.call(
|
|
168
205
|
"powershell",
|
|
169
206
|
"-NoProfile",
|
|
170
207
|
"-Command",
|
|
@@ -173,7 +210,7 @@ module WGPU
|
|
|
173
210
|
end
|
|
174
211
|
|
|
175
212
|
def extract_with_unzip(archive_path, destination)
|
|
176
|
-
|
|
213
|
+
@command_runner.call("unzip", "-o", "-q", archive_path, "-d", destination)
|
|
177
214
|
end
|
|
178
215
|
|
|
179
216
|
def windows?
|
|
@@ -182,7 +219,7 @@ module WGPU
|
|
|
182
219
|
|
|
183
220
|
def recovery_instructions
|
|
184
221
|
<<~INSTRUCTIONS.chomp
|
|
185
|
-
Download the matching #{
|
|
222
|
+
Download the matching #{@distribution.const_get(:VERSION)} artifact manually and set:
|
|
186
223
|
WGPU_LIB_PATH=/absolute/path/to/the/wgpu-native/shared-library
|
|
187
224
|
See docs/installation.md for platform details.
|
|
188
225
|
INSTRUCTIONS
|
data/lib/wgpu/native/loader.rb
CHANGED
|
@@ -6,6 +6,12 @@ require_relative "distribution"
|
|
|
6
6
|
module WGPU
|
|
7
7
|
module Native
|
|
8
8
|
module OptionalFunctions
|
|
9
|
+
# Attaches a native function without failing library initialization.
|
|
10
|
+
#
|
|
11
|
+
# @param name [Symbol] exported function name
|
|
12
|
+
# @param arguments [Array] FFI argument types
|
|
13
|
+
# @param result [Symbol, FFI::Type] FFI return type
|
|
14
|
+
# @return [void]
|
|
9
15
|
def attach_optional_function(name, arguments, result)
|
|
10
16
|
attach_function(name, arguments, result)
|
|
11
17
|
optional_functions[name] = true
|
|
@@ -18,10 +24,16 @@ module WGPU
|
|
|
18
24
|
end
|
|
19
25
|
end
|
|
20
26
|
|
|
27
|
+
# Reports whether an optional native function was attached.
|
|
28
|
+
# @param name [Symbol] exported function name
|
|
29
|
+
# @return [Boolean]
|
|
21
30
|
def optional_function_available?(name)
|
|
22
31
|
optional_functions.fetch(name, false)
|
|
23
32
|
end
|
|
24
33
|
|
|
34
|
+
# Returns availability information for optional native functions.
|
|
35
|
+
#
|
|
36
|
+
# @return [Hash{Symbol => Boolean}] immutable capability snapshot
|
|
25
37
|
def optional_capabilities
|
|
26
38
|
optional_functions.dup.freeze
|
|
27
39
|
end
|
|
@@ -37,6 +49,9 @@ module WGPU
|
|
|
37
49
|
extend OptionalFunctions
|
|
38
50
|
|
|
39
51
|
class << self
|
|
52
|
+
# Resolves the native library from an override or the versioned cache.
|
|
53
|
+
# @return [String] absolute shared-library path
|
|
54
|
+
# @raise [LoadError] when no library can be found
|
|
40
55
|
def library_path
|
|
41
56
|
if ENV["WGPU_LIB_PATH"] && !ENV["WGPU_LIB_PATH"].empty?
|
|
42
57
|
path = File.expand_path(ENV["WGPU_LIB_PATH"])
|
|
@@ -60,10 +75,17 @@ module WGPU
|
|
|
60
75
|
MSG
|
|
61
76
|
end
|
|
62
77
|
|
|
78
|
+
# Returns the preferred native-library cache directory.
|
|
79
|
+
#
|
|
80
|
+
# @return [String] absolute cache directory
|
|
63
81
|
def cache_dir
|
|
64
82
|
Distribution.primary_cache_dir
|
|
65
83
|
end
|
|
66
84
|
|
|
85
|
+
# Returns the shared-library filename for this platform.
|
|
86
|
+
#
|
|
87
|
+
# @return [String] platform-specific filename
|
|
88
|
+
# @raise [LoadError] if the platform is unsupported
|
|
67
89
|
def library_name
|
|
68
90
|
Distribution.artifact_for.fetch(:library)
|
|
69
91
|
end
|
data/lib/wgpu/native/structs.rb
CHANGED
|
@@ -378,6 +378,22 @@ module WGPU
|
|
|
378
378
|
:userdata2, :pointer
|
|
379
379
|
end
|
|
380
380
|
|
|
381
|
+
class CreateComputePipelineAsyncCallbackInfo < FFI::Struct
|
|
382
|
+
layout :next_in_chain, :pointer,
|
|
383
|
+
:mode, :uint32,
|
|
384
|
+
:callback, :pointer,
|
|
385
|
+
:userdata1, :pointer,
|
|
386
|
+
:userdata2, :pointer
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
class CreateRenderPipelineAsyncCallbackInfo < FFI::Struct
|
|
390
|
+
layout :next_in_chain, :pointer,
|
|
391
|
+
:mode, :uint32,
|
|
392
|
+
:callback, :pointer,
|
|
393
|
+
:userdata1, :pointer,
|
|
394
|
+
:userdata2, :pointer
|
|
395
|
+
end
|
|
396
|
+
|
|
381
397
|
class BufferMapCallbackInfo < FFI::Struct
|
|
382
398
|
layout :next_in_chain, :pointer,
|
|
383
399
|
:mode, :uint32,
|
|
@@ -395,7 +411,8 @@ module WGPU
|
|
|
395
411
|
:mip_level_count, :uint32,
|
|
396
412
|
:base_array_layer, :uint32,
|
|
397
413
|
:array_layer_count, :uint32,
|
|
398
|
-
:aspect, TextureAspect
|
|
414
|
+
:aspect, TextureAspect,
|
|
415
|
+
:usage, :uint64
|
|
399
416
|
end
|
|
400
417
|
|
|
401
418
|
class SamplerDescriptor < FFI::Struct
|
data/lib/wgpu/native_resource.rb
CHANGED
|
@@ -13,6 +13,10 @@ module WGPU
|
|
|
13
13
|
|
|
14
14
|
module_function
|
|
15
15
|
|
|
16
|
+
# Registers a live native resource when leak debugging is enabled.
|
|
17
|
+
#
|
|
18
|
+
# @param resource [NativeResource] resource to track
|
|
19
|
+
# @return [void]
|
|
16
20
|
def register(resource)
|
|
17
21
|
return unless WGPU.debug_leaks
|
|
18
22
|
|
|
@@ -22,10 +26,17 @@ module WGPU
|
|
|
22
26
|
ObjectSpace.define_finalizer(resource, finalizer(object_id))
|
|
23
27
|
end
|
|
24
28
|
|
|
29
|
+
# Removes a resource from leak tracking.
|
|
30
|
+
#
|
|
31
|
+
# @param resource [NativeResource] resource that was released
|
|
32
|
+
# @return [void]
|
|
25
33
|
def unregister(resource)
|
|
26
34
|
@mutex.synchronize { @resources.delete(resource.object_id) }
|
|
27
35
|
end
|
|
28
36
|
|
|
37
|
+
# Warns about and clears every tracked resource.
|
|
38
|
+
#
|
|
39
|
+
# @return [void]
|
|
29
40
|
def warn_remaining
|
|
30
41
|
resources = @mutex.synchronize do
|
|
31
42
|
remaining = @resources.values
|
|
@@ -53,25 +64,100 @@ module WGPU
|
|
|
53
64
|
|
|
54
65
|
at_exit { LeakTracker.warn_remaining if WGPU.debug_leaks }
|
|
55
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
|
+
|
|
56
105
|
module CallbackKeepalive
|
|
57
106
|
INITIALIZATION_MUTEX = Mutex.new
|
|
107
|
+
RETAINED_MUTEX = Mutex.new
|
|
108
|
+
RETAINED_CALLBACKS = {}
|
|
58
109
|
|
|
59
110
|
module_function
|
|
60
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}
|
|
61
117
|
def retain(owner, callback)
|
|
62
118
|
mutex, callbacks = storage_for(owner)
|
|
63
119
|
token = Object.new
|
|
64
120
|
mutex.synchronize { callbacks[token] = callback }
|
|
121
|
+
RETAINED_MUTEX.synchronize { RETAINED_CALLBACKS[token] = callback }
|
|
65
122
|
token
|
|
66
123
|
end
|
|
67
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]
|
|
68
130
|
def release(owner, token)
|
|
69
131
|
return unless token
|
|
70
132
|
|
|
71
133
|
mutex, callbacks = storage_for(owner)
|
|
72
134
|
mutex.synchronize { callbacks.delete(token) }
|
|
135
|
+
RETAINED_MUTEX.synchronize { RETAINED_CALLBACKS.delete(token) }
|
|
73
136
|
end
|
|
74
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
|
|
75
161
|
def count(owner)
|
|
76
162
|
mutex, callbacks = storage_for(owner)
|
|
77
163
|
mutex.synchronize { callbacks.length }
|
|
@@ -96,22 +182,65 @@ module WGPU
|
|
|
96
182
|
module NativeResource
|
|
97
183
|
GUARDED_METHOD_EXEMPTIONS = [:initialize, :release, :released?, :handle, :label, :inspect].freeze
|
|
98
184
|
|
|
185
|
+
# Validates resource arguments before passing their handles to FFI.
|
|
186
|
+
def self.checked_handle(resource, expected_class:)
|
|
187
|
+
unless resource.is_a?(expected_class)
|
|
188
|
+
raise TypeError, "expected #{expected_class}, got #{resource.class}"
|
|
189
|
+
end
|
|
190
|
+
resource.send(:ensure_not_released!)
|
|
191
|
+
resource.handle
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
module ClassMethods
|
|
195
|
+
private
|
|
196
|
+
|
|
197
|
+
def adopt_native_handle(handle, label: nil)
|
|
198
|
+
resource = allocate
|
|
199
|
+
resource.send(:initialize_native_resource, handle: handle, label: label)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
99
203
|
module Lifecycle
|
|
204
|
+
UNSET = Object.new.freeze
|
|
205
|
+
private_constant :UNSET
|
|
206
|
+
|
|
207
|
+
# Initializes a wrapper and registers its native-resource lifecycle.
|
|
208
|
+
#
|
|
209
|
+
# @param args [Array] positional arguments forwarded to the resource
|
|
210
|
+
# @param kwargs [Hash] keyword arguments forwarded to the resource
|
|
211
|
+
# @yield block forwarded to the resource initializer
|
|
212
|
+
# @return [NativeResource] initialized resource
|
|
100
213
|
def initialize(*args, **kwargs, &block)
|
|
101
214
|
super
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
215
|
+
initialize_native_resource(label: kwargs.fetch(:label, UNSET))
|
|
216
|
+
attach_device_callback_lifetime_from_parent
|
|
217
|
+
initialized = true
|
|
218
|
+
ensure
|
|
219
|
+
release if !initialized && @handle && !released?
|
|
105
220
|
end
|
|
106
221
|
|
|
222
|
+
# Releases a resource once and unregisters it from leak tracking.
|
|
223
|
+
#
|
|
224
|
+
# @return [void]
|
|
107
225
|
def release(...)
|
|
108
226
|
return if released?
|
|
109
227
|
|
|
110
228
|
result = super
|
|
229
|
+
release_device_callback_lifetime
|
|
111
230
|
@released = true
|
|
112
231
|
LeakTracker.unregister(self)
|
|
113
232
|
result
|
|
114
233
|
end
|
|
234
|
+
|
|
235
|
+
private
|
|
236
|
+
|
|
237
|
+
def initialize_native_resource(handle: UNSET, label: UNSET)
|
|
238
|
+
@handle = handle unless handle.equal?(UNSET)
|
|
239
|
+
@label = label unless label.equal?(UNSET)
|
|
240
|
+
@released = false
|
|
241
|
+
LeakTracker.register(self)
|
|
242
|
+
self
|
|
243
|
+
end
|
|
115
244
|
end
|
|
116
245
|
|
|
117
246
|
def self.included(base)
|
|
@@ -124,12 +253,15 @@ module WGPU
|
|
|
124
253
|
super(*args, **kwargs, &block)
|
|
125
254
|
end
|
|
126
255
|
end
|
|
256
|
+
base.extend(ClassMethods)
|
|
127
257
|
base.prepend(guard)
|
|
128
258
|
base.prepend(Lifecycle)
|
|
129
259
|
end
|
|
130
260
|
|
|
131
261
|
attr_reader :label
|
|
132
262
|
|
|
263
|
+
# Reports whether the native handle has been released or is null.
|
|
264
|
+
# @return [Boolean]
|
|
133
265
|
def released?
|
|
134
266
|
return true if @released
|
|
135
267
|
return false unless instance_variable_defined?(:@handle)
|
|
@@ -154,6 +286,9 @@ module WGPU
|
|
|
154
286
|
release if block_given? && !released?
|
|
155
287
|
end
|
|
156
288
|
|
|
289
|
+
# Returns a concise lifecycle-oriented representation.
|
|
290
|
+
#
|
|
291
|
+
# @return [String] class, optional label, and release state
|
|
157
292
|
def inspect
|
|
158
293
|
label_text = @label ? " label=#{@label.inspect}" : ""
|
|
159
294
|
"#<#{self.class}#{label_text} released=#{released?}>"
|
|
@@ -161,6 +296,58 @@ module WGPU
|
|
|
161
296
|
|
|
162
297
|
private
|
|
163
298
|
|
|
299
|
+
def initialize_copy(_source)
|
|
300
|
+
raise TypeError, "#{self.class} owns a native resource and cannot be copied"
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def attach_device_callback_lifetime(owner)
|
|
304
|
+
lifetime = owner&.instance_variable_get(:@device_callback_lifetime)
|
|
305
|
+
return self unless lifetime
|
|
306
|
+
|
|
307
|
+
lifetime.retain
|
|
308
|
+
@device_callback_lifetime = lifetime
|
|
309
|
+
@device_callback_lifetime_retained = true
|
|
310
|
+
self
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def attach_device_callback_lifetime_from_parent
|
|
314
|
+
return if @device_callback_lifetime_retained
|
|
315
|
+
|
|
316
|
+
if @device_callback_lifetime
|
|
317
|
+
@device_callback_lifetime.retain
|
|
318
|
+
@device_callback_lifetime_retained = true
|
|
319
|
+
return
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
owner = @device || @encoder || @texture
|
|
323
|
+
attach_device_callback_lifetime(owner)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def release_device_callback_lifetime
|
|
327
|
+
return unless @device_callback_lifetime_retained
|
|
328
|
+
|
|
329
|
+
@device_callback_lifetime_retained = false
|
|
330
|
+
@device_callback_lifetime.release
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def device_callback_lifetime_lease
|
|
334
|
+
lifetime = @device_callback_lifetime
|
|
335
|
+
return proc {} unless lifetime
|
|
336
|
+
|
|
337
|
+
lifetime.retain
|
|
338
|
+
released = false
|
|
339
|
+
mutex = Mutex.new
|
|
340
|
+
proc do
|
|
341
|
+
should_release = mutex.synchronize do
|
|
342
|
+
next false if released
|
|
343
|
+
|
|
344
|
+
released = true
|
|
345
|
+
true
|
|
346
|
+
end
|
|
347
|
+
lifetime.release if should_release
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
164
351
|
def ensure_not_released!
|
|
165
352
|
return unless released?
|
|
166
353
|
|
|
@@ -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
|
|
|
@@ -24,13 +29,13 @@ module WGPU
|
|
|
24
29
|
desc[:label][:data] = nil
|
|
25
30
|
desc[:label][:length] = 0
|
|
26
31
|
end
|
|
27
|
-
desc[:layout] = layout
|
|
32
|
+
desc[:layout] = NativeResource.checked_handle(layout, expected_class: BindGroupLayout)
|
|
28
33
|
desc[:entry_count] = entries_array.size
|
|
29
34
|
desc[:entries] = entries_ptr
|
|
30
35
|
|
|
31
|
-
device.
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
error = device.send(:capture_error_scope) do
|
|
37
|
+
@handle = Native.wgpuDeviceCreateBindGroup(NativeResource.checked_handle(device, expected_class: Device), desc)
|
|
38
|
+
end
|
|
34
39
|
|
|
35
40
|
if @handle.null? || (error[:type] && error[:type] != :no_error)
|
|
36
41
|
msg = error[:message] || "Failed to create bind group"
|
|
@@ -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)
|
|
@@ -65,7 +74,7 @@ module WGPU
|
|
|
65
74
|
|
|
66
75
|
if entry_hash[:buffer]
|
|
67
76
|
buffer = entry_hash[:buffer]
|
|
68
|
-
entry[:buffer] = buffer
|
|
77
|
+
entry[:buffer] = NativeResource.checked_handle(buffer, expected_class: Buffer)
|
|
69
78
|
entry[:offset] = entry_hash[:offset] || 0
|
|
70
79
|
entry[:size] = entry_hash[:size] || buffer.size
|
|
71
80
|
else
|
|
@@ -75,13 +84,13 @@ module WGPU
|
|
|
75
84
|
end
|
|
76
85
|
|
|
77
86
|
if entry_hash[:sampler]
|
|
78
|
-
entry[:sampler] = entry_hash[:sampler]
|
|
87
|
+
entry[:sampler] = NativeResource.checked_handle(entry_hash[:sampler], expected_class: Sampler)
|
|
79
88
|
else
|
|
80
89
|
entry[:sampler] = nil
|
|
81
90
|
end
|
|
82
91
|
|
|
83
92
|
if entry_hash[:texture_view]
|
|
84
|
-
entry[:texture_view] = entry_hash[:texture_view]
|
|
93
|
+
entry[:texture_view] = NativeResource.checked_handle(entry_hash[:texture_view], expected_class: TextureView)
|
|
85
94
|
else
|
|
86
95
|
entry[:texture_view] = nil
|
|
87
96
|
end
|
|
@@ -4,20 +4,29 @@ 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
25
|
desc, @descriptor_keepalive = build_descriptor(label:, entries:)
|
|
17
26
|
|
|
18
|
-
device.
|
|
19
|
-
|
|
20
|
-
|
|
27
|
+
error = device.send(:capture_error_scope) do
|
|
28
|
+
@handle = Native.wgpuDeviceCreateBindGroupLayout(NativeResource.checked_handle(device, expected_class: Device), desc)
|
|
29
|
+
end
|
|
21
30
|
@descriptor_keepalive = nil
|
|
22
31
|
|
|
23
32
|
if @handle.null? || (error[:type] && error[:type] != :no_error)
|
|
@@ -26,6 +35,10 @@ module WGPU
|
|
|
26
35
|
end
|
|
27
36
|
end
|
|
28
37
|
|
|
38
|
+
# Releases the native bind group layout handle.
|
|
39
|
+
#
|
|
40
|
+
# Calling this method more than once has no effect.
|
|
41
|
+
# @return [void]
|
|
29
42
|
def release
|
|
30
43
|
return if @handle.null?
|
|
31
44
|
Native.wgpuBindGroupLayoutRelease(@handle)
|