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
|
@@ -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
|
|
|
@@ -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)
|
|
@@ -164,7 +195,7 @@ module WGPU
|
|
|
164
195
|
end
|
|
165
196
|
|
|
166
197
|
def extract_with_powershell(archive_path, destination)
|
|
167
|
-
|
|
198
|
+
@command_runner.call(
|
|
168
199
|
"powershell",
|
|
169
200
|
"-NoProfile",
|
|
170
201
|
"-Command",
|
|
@@ -173,7 +204,7 @@ module WGPU
|
|
|
173
204
|
end
|
|
174
205
|
|
|
175
206
|
def extract_with_unzip(archive_path, destination)
|
|
176
|
-
|
|
207
|
+
@command_runner.call("unzip", "-o", "-q", archive_path, "-d", destination)
|
|
177
208
|
end
|
|
178
209
|
|
|
179
210
|
def windows?
|
|
@@ -182,7 +213,7 @@ module WGPU
|
|
|
182
213
|
|
|
183
214
|
def recovery_instructions
|
|
184
215
|
<<~INSTRUCTIONS.chomp
|
|
185
|
-
Download the matching #{
|
|
216
|
+
Download the matching #{@distribution.const_get(:VERSION)} artifact manually and set:
|
|
186
217
|
WGPU_LIB_PATH=/absolute/path/to/the/wgpu-native/shared-library
|
|
187
218
|
See docs/installation.md for platform details.
|
|
188
219
|
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) }
|
|
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
|
|
73
155
|
end
|
|
74
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,53 @@ module WGPU
|
|
|
96
182
|
module NativeResource
|
|
97
183
|
GUARDED_METHOD_EXEMPTIONS = [:initialize, :release, :released?, :handle, :label, :inspect].freeze
|
|
98
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
|
+
|
|
99
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
|
|
100
204
|
def initialize(*args, **kwargs, &block)
|
|
101
205
|
super
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
LeakTracker.register(self)
|
|
206
|
+
initialize_native_resource(label: kwargs.fetch(:label, UNSET))
|
|
207
|
+
attach_device_callback_lifetime_from_parent
|
|
105
208
|
end
|
|
106
209
|
|
|
210
|
+
# Releases a resource once and unregisters it from leak tracking.
|
|
211
|
+
#
|
|
212
|
+
# @return [void]
|
|
107
213
|
def release(...)
|
|
108
214
|
return if released?
|
|
109
215
|
|
|
110
216
|
result = super
|
|
217
|
+
release_device_callback_lifetime
|
|
111
218
|
@released = true
|
|
112
219
|
LeakTracker.unregister(self)
|
|
113
220
|
result
|
|
114
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
|
|
115
232
|
end
|
|
116
233
|
|
|
117
234
|
def self.included(base)
|
|
@@ -124,12 +241,15 @@ module WGPU
|
|
|
124
241
|
super(*args, **kwargs, &block)
|
|
125
242
|
end
|
|
126
243
|
end
|
|
244
|
+
base.extend(ClassMethods)
|
|
127
245
|
base.prepend(guard)
|
|
128
246
|
base.prepend(Lifecycle)
|
|
129
247
|
end
|
|
130
248
|
|
|
131
249
|
attr_reader :label
|
|
132
250
|
|
|
251
|
+
# Reports whether the native handle has been released or is null.
|
|
252
|
+
# @return [Boolean]
|
|
133
253
|
def released?
|
|
134
254
|
return true if @released
|
|
135
255
|
return false unless instance_variable_defined?(:@handle)
|
|
@@ -154,6 +274,9 @@ module WGPU
|
|
|
154
274
|
release if block_given? && !released?
|
|
155
275
|
end
|
|
156
276
|
|
|
277
|
+
# Returns a concise lifecycle-oriented representation.
|
|
278
|
+
#
|
|
279
|
+
# @return [String] class, optional label, and release state
|
|
157
280
|
def inspect
|
|
158
281
|
label_text = @label ? " label=#{@label.inspect}" : ""
|
|
159
282
|
"#<#{self.class}#{label_text} released=#{released?}>"
|
|
@@ -161,6 +284,54 @@ module WGPU
|
|
|
161
284
|
|
|
162
285
|
private
|
|
163
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
|
+
|
|
164
335
|
def ensure_not_released!
|
|
165
336
|
return unless released?
|
|
166
337
|
|
|
@@ -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)
|
|
@@ -4,13 +4,22 @@ 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:)
|
|
@@ -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)
|
|
@@ -4,6 +4,11 @@ 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
14
|
desc, @pointers = build_descriptor(label:, layout:, compute:)
|
|
@@ -18,12 +23,20 @@ module WGPU
|
|
|
18
23
|
end
|
|
19
24
|
end
|
|
20
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
|
|
21
30
|
def get_bind_group_layout(index)
|
|
22
31
|
handle = Native.wgpuComputePipelineGetBindGroupLayout(@handle, index)
|
|
23
32
|
raise PipelineError, "Failed to get bind group layout at index #{index}" if handle.null?
|
|
24
|
-
BindGroupLayout.from_handle(handle)
|
|
33
|
+
BindGroupLayout.from_handle(handle, device: @device)
|
|
25
34
|
end
|
|
26
35
|
|
|
36
|
+
# Releases the native compute pipeline handle.
|
|
37
|
+
#
|
|
38
|
+
# Calling this method more than once has no effect.
|
|
39
|
+
# @return [void]
|
|
27
40
|
def release
|
|
28
41
|
return if @handle.null?
|
|
29
42
|
Native.wgpuComputePipelineRelease(@handle)
|
|
@@ -40,9 +53,6 @@ module WGPU
|
|
|
40
53
|
context: "compute pipeline stage"
|
|
41
54
|
)
|
|
42
55
|
@pointers = []
|
|
43
|
-
entry_point = compute[:entry_point] || "main"
|
|
44
|
-
entry_point_ptr = FFI::MemoryPointer.from_string(entry_point)
|
|
45
|
-
@pointers << entry_point_ptr
|
|
46
56
|
|
|
47
57
|
desc = Native::ComputePipelineDescriptor.new
|
|
48
58
|
desc[:next_in_chain] = nil
|
|
@@ -50,11 +60,12 @@ module WGPU
|
|
|
50
60
|
desc[:layout] = normalize_layout(layout)
|
|
51
61
|
desc[:compute][:next_in_chain] = nil
|
|
52
62
|
desc[:compute][:module] = compute.fetch(:module).handle
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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)
|
|
58
69
|
|
|
59
70
|
[desc, @pointers]
|
|
60
71
|
end
|
|
@@ -63,28 +74,5 @@ module WGPU
|
|
|
63
74
|
return nil if layout.nil? || layout == :auto || layout == "auto"
|
|
64
75
|
layout.handle
|
|
65
76
|
end
|
|
66
|
-
|
|
67
|
-
def setup_constants(stage_desc, constants)
|
|
68
|
-
return if constants.nil? || constants.empty?
|
|
69
|
-
|
|
70
|
-
constants_ptr = FFI::MemoryPointer.new(Native::ConstantEntry, constants.size)
|
|
71
|
-
@pointers << constants_ptr
|
|
72
|
-
|
|
73
|
-
constants.each_with_index do |(key, value), i|
|
|
74
|
-
entry_ptr = constants_ptr + (i * Native::ConstantEntry.size)
|
|
75
|
-
entry = Native::ConstantEntry.new(entry_ptr)
|
|
76
|
-
entry[:next_in_chain] = nil
|
|
77
|
-
|
|
78
|
-
key_str = key.to_s
|
|
79
|
-
key_ptr = FFI::MemoryPointer.from_string(key_str)
|
|
80
|
-
@pointers << key_ptr
|
|
81
|
-
entry[:key][:data] = key_ptr
|
|
82
|
-
entry[:key][:length] = key_str.bytesize
|
|
83
|
-
entry[:value] = value.to_f
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
stage_desc[:constant_count] = constants.size
|
|
87
|
-
stage_desc[:constants] = constants_ptr
|
|
88
|
-
end
|
|
89
77
|
end
|
|
90
78
|
end
|