wgpu 1.1.0 → 1.2.0
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 +48 -0
- data/README.md +26 -3
- data/docs/README.md +22 -0
- data/docs/api_coverage.md +132 -0
- data/docs/async.md +31 -0
- data/docs/bind_groups.md +25 -0
- data/docs/buffer_data.md +37 -0
- data/docs/command_encoding.md +24 -0
- data/docs/errors.md +40 -0
- data/docs/getting_started_compute.md +61 -0
- data/docs/getting_started_rendering.md +62 -0
- data/docs/installation.md +94 -0
- data/docs/pipeline_descriptors.md +38 -0
- data/docs/releasing.md +22 -0
- data/docs/resource_lifetime.md +94 -0
- data/docs/shaders.md +32 -0
- data/docs/texture_readback.md +19 -0
- data/docs/troubleshooting.md +48 -0
- data/docs/upgrading_wgpu_native.md +26 -0
- data/ext/wgpu/extconf.rb +10 -142
- data/lib/wgpu/commands/command_buffer.rb +11 -0
- data/lib/wgpu/commands/command_encoder.rb +65 -8
- data/lib/wgpu/commands/compute_pass.rb +10 -0
- data/lib/wgpu/commands/render_bundle_encoder.rb +10 -3
- data/lib/wgpu/commands/render_pass.rb +34 -8
- data/lib/wgpu/core/adapter.rb +36 -11
- data/lib/wgpu/core/async_waiter.rb +32 -4
- data/lib/wgpu/core/canvas_context.rb +0 -2
- data/lib/wgpu/core/device.rb +138 -42
- data/lib/wgpu/core/instance.rb +8 -4
- data/lib/wgpu/core/queue.rb +80 -49
- data/lib/wgpu/core/surface.rb +16 -17
- data/lib/wgpu/data_types.rb +67 -0
- data/lib/wgpu/descriptor_helpers.rb +39 -0
- data/lib/wgpu/error.rb +55 -0
- data/lib/wgpu/logging.rb +63 -0
- data/lib/wgpu/native/abi_verifier.rb +109 -0
- data/lib/wgpu/native/callbacks.rb +4 -1
- data/lib/wgpu/native/capabilities.rb +18 -2
- data/lib/wgpu/native/distribution.rb +113 -0
- data/lib/wgpu/native/enum_helper.rb +63 -0
- data/lib/wgpu/native/enums.rb +60 -8
- data/lib/wgpu/native/functions.rb +10 -10
- data/lib/wgpu/native/installer.rb +192 -0
- data/lib/wgpu/native/loader.rb +39 -21
- data/lib/wgpu/native_resource.rb +171 -0
- data/lib/wgpu/pipeline/bind_group.rb +12 -0
- data/lib/wgpu/pipeline/bind_group_layout.rb +91 -37
- data/lib/wgpu/pipeline/compute_pipeline.rb +28 -26
- data/lib/wgpu/pipeline/render_pipeline.rb +180 -68
- data/lib/wgpu/pipeline/shader_module.rb +50 -8
- data/lib/wgpu/resources/buffer.rb +148 -73
- data/lib/wgpu/resources/query_set.rb +14 -12
- data/lib/wgpu/resources/sampler.rb +36 -20
- data/lib/wgpu/resources/texture.rb +44 -42
- data/lib/wgpu/resources/texture_view.rb +11 -3
- data/lib/wgpu/texture_format.rb +82 -0
- data/lib/wgpu/version.rb +1 -1
- data/lib/wgpu/window.rb +8 -1
- data/lib/wgpu.rb +32 -0
- data/sig/wgpu.rbs +381 -0
- metadata +31 -16
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
require_relative "distribution"
|
|
9
|
+
|
|
10
|
+
module WGPU
|
|
11
|
+
module Native
|
|
12
|
+
class InstallError < StandardError; end
|
|
13
|
+
|
|
14
|
+
class Installer
|
|
15
|
+
attr_reader :platform
|
|
16
|
+
|
|
17
|
+
def initialize(platform: RUBY_PLATFORM, env: ENV, home: Dir.home,
|
|
18
|
+
host_os: RbConfig::CONFIG["host_os"], output: $stdout)
|
|
19
|
+
@platform = platform
|
|
20
|
+
@env = env
|
|
21
|
+
@home = home
|
|
22
|
+
@host_os = host_os
|
|
23
|
+
@output = output
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def install
|
|
27
|
+
return custom_library_path if custom_library_path
|
|
28
|
+
|
|
29
|
+
artifact = Distribution.artifact_for(platform)
|
|
30
|
+
cached_path = existing_library_path(artifact)
|
|
31
|
+
if cached_path
|
|
32
|
+
@output.puts "wgpu-native already cached at #{cached_path}"
|
|
33
|
+
return cached_path
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
install_artifact(artifact)
|
|
37
|
+
rescue LoadError, InstallError => error
|
|
38
|
+
raise InstallError, "#{error.message}\n\n#{recovery_instructions}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def clean_path
|
|
42
|
+
Distribution.primary_cache_dir(env: @env, home: @home, host_os: @host_os)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def custom_library_path
|
|
48
|
+
path = @env["WGPU_LIB_PATH"]
|
|
49
|
+
return if path.nil? || path.empty?
|
|
50
|
+
raise InstallError, "WGPU_LIB_PATH points to a non-existent file: #{path}" unless File.file?(path)
|
|
51
|
+
|
|
52
|
+
File.expand_path(path)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def existing_library_path(artifact)
|
|
56
|
+
Distribution.cache_directories(env: @env, home: @home, host_os: @host_os).each do |directory|
|
|
57
|
+
path = File.join(directory, "lib", artifact.fetch(:library))
|
|
58
|
+
return path if File.file?(path)
|
|
59
|
+
end
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def install_artifact(artifact)
|
|
64
|
+
cache_dir = clean_path
|
|
65
|
+
archive_path = File.join(cache_dir, artifact.fetch(:archive))
|
|
66
|
+
library_path = File.join(cache_dir, "lib", artifact.fetch(:library))
|
|
67
|
+
FileUtils.mkdir_p(cache_dir)
|
|
68
|
+
|
|
69
|
+
@output.puts "Downloading wgpu-native #{Distribution::VERSION} from GitHub..."
|
|
70
|
+
@output.puts " URL: #{Distribution.release_url(artifact)}"
|
|
71
|
+
download_file(Distribution.release_url(artifact), archive_path)
|
|
72
|
+
verify_checksum!(archive_path, artifact.fetch(:sha256))
|
|
73
|
+
|
|
74
|
+
@output.puts "Extracting to #{cache_dir}..."
|
|
75
|
+
extract_zip(archive_path, cache_dir)
|
|
76
|
+
raise InstallError, "Archive did not contain lib/#{artifact.fetch(:library)}" unless File.file?(library_path)
|
|
77
|
+
|
|
78
|
+
@output.puts "wgpu-native installed successfully!"
|
|
79
|
+
library_path
|
|
80
|
+
ensure
|
|
81
|
+
FileUtils.rm_f(archive_path) if archive_path
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def verify_checksum!(path, expected)
|
|
85
|
+
actual = Digest::SHA256.file(path).hexdigest
|
|
86
|
+
return if actual == expected
|
|
87
|
+
|
|
88
|
+
FileUtils.rm_f(path)
|
|
89
|
+
raise InstallError, "SHA-256 mismatch for #{File.basename(path)} (expected #{expected}, got #{actual})"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def download_file(url, destination)
|
|
93
|
+
return if curl_available? && download_with_curl(url, destination)
|
|
94
|
+
return if download_with_ruby(url, destination)
|
|
95
|
+
|
|
96
|
+
raise InstallError, "Download failed: #{url}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def curl_available?
|
|
100
|
+
system("curl", "--version", out: File::NULL, err: File::NULL)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def download_with_curl(url, destination)
|
|
104
|
+
system("curl", "-fsSL", "-o", destination, url)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def download_with_ruby(url, destination, redirects = 5)
|
|
108
|
+
raise InstallError, "Too many redirects while downloading #{url}" if redirects.zero?
|
|
109
|
+
|
|
110
|
+
uri = URI.parse(url)
|
|
111
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
112
|
+
http.use_ssl = uri.scheme == "https"
|
|
113
|
+
http.open_timeout = 10
|
|
114
|
+
http.read_timeout = 120
|
|
115
|
+
|
|
116
|
+
http.request(Net::HTTP::Get.new(uri.request_uri)) do |response|
|
|
117
|
+
case response
|
|
118
|
+
when Net::HTTPRedirection
|
|
119
|
+
location = response["location"]
|
|
120
|
+
raise InstallError, "Redirect response did not include a location" unless location
|
|
121
|
+
|
|
122
|
+
location = URI.join(url, location).to_s
|
|
123
|
+
return download_with_ruby(location, destination, redirects - 1)
|
|
124
|
+
when Net::HTTPSuccess
|
|
125
|
+
File.open(destination, "wb") do |file|
|
|
126
|
+
response.read_body { |chunk| file.write(chunk) }
|
|
127
|
+
end
|
|
128
|
+
return true
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
false
|
|
133
|
+
rescue SystemCallError, SocketError, URI::InvalidURIError => error
|
|
134
|
+
@output.puts "Ruby download failed: #{error.message}"
|
|
135
|
+
false
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def extract_zip(archive_path, destination)
|
|
139
|
+
return if extract_with_rubyzip(archive_path, destination)
|
|
140
|
+
return if windows? && extract_with_powershell(archive_path, destination)
|
|
141
|
+
return if extract_with_unzip(archive_path, destination)
|
|
142
|
+
|
|
143
|
+
raise InstallError, "Failed to extract zip; install the rubyzip gem or a supported system extractor"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def extract_with_rubyzip(archive_path, destination)
|
|
147
|
+
require "zip"
|
|
148
|
+
destination_root = "#{File.expand_path(destination)}#{File::SEPARATOR}"
|
|
149
|
+
|
|
150
|
+
Zip::File.open(archive_path) do |zip_file|
|
|
151
|
+
zip_file.each do |entry|
|
|
152
|
+
target = File.expand_path(entry.name, destination)
|
|
153
|
+
unless target.start_with?(destination_root)
|
|
154
|
+
raise InstallError, "Archive entry escapes destination: #{entry.name}"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
FileUtils.mkdir_p(entry.directory? ? target : File.dirname(target))
|
|
158
|
+
entry.extract(target) { true } unless entry.directory?
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
true
|
|
162
|
+
rescue LoadError
|
|
163
|
+
false
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def extract_with_powershell(archive_path, destination)
|
|
167
|
+
system(
|
|
168
|
+
"powershell",
|
|
169
|
+
"-NoProfile",
|
|
170
|
+
"-Command",
|
|
171
|
+
"Expand-Archive -Force -LiteralPath '#{archive_path}' -DestinationPath '#{destination}'"
|
|
172
|
+
)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def extract_with_unzip(archive_path, destination)
|
|
176
|
+
system("unzip", "-o", "-q", archive_path, "-d", destination)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def windows?
|
|
180
|
+
/mingw|mswin/.match?(platform)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def recovery_instructions
|
|
184
|
+
<<~INSTRUCTIONS.chomp
|
|
185
|
+
Download the matching #{Distribution::VERSION} artifact manually and set:
|
|
186
|
+
WGPU_LIB_PATH=/absolute/path/to/the/wgpu-native/shared-library
|
|
187
|
+
See docs/installation.md for platform details.
|
|
188
|
+
INSTRUCTIONS
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
data/lib/wgpu/native/loader.rb
CHANGED
|
@@ -1,53 +1,71 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "ffi"
|
|
4
|
-
|
|
4
|
+
require_relative "distribution"
|
|
5
5
|
|
|
6
6
|
module WGPU
|
|
7
7
|
module Native
|
|
8
|
-
|
|
8
|
+
module OptionalFunctions
|
|
9
|
+
def attach_optional_function(name, arguments, result)
|
|
10
|
+
attach_function(name, arguments, result)
|
|
11
|
+
optional_functions[name] = true
|
|
12
|
+
rescue FFI::NotFoundError
|
|
13
|
+
optional_functions[name] = false
|
|
14
|
+
define_singleton_method(name) do |*|
|
|
15
|
+
raise WGPU::Error,
|
|
16
|
+
"Optional wgpu-native function #{name} is unavailable in the loaded library " \
|
|
17
|
+
"(expected #{Distribution::VERSION})"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def optional_function_available?(name)
|
|
22
|
+
optional_functions.fetch(name, false)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def optional_capabilities
|
|
26
|
+
optional_functions.dup.freeze
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
9
30
|
|
|
10
|
-
|
|
31
|
+
def optional_functions
|
|
32
|
+
@optional_functions ||= {}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
extend FFI::Library
|
|
37
|
+
extend OptionalFunctions
|
|
11
38
|
|
|
12
39
|
class << self
|
|
13
40
|
def library_path
|
|
14
|
-
if ENV["WGPU_LIB_PATH"]
|
|
15
|
-
path = ENV["WGPU_LIB_PATH"]
|
|
41
|
+
if ENV["WGPU_LIB_PATH"] && !ENV["WGPU_LIB_PATH"].empty?
|
|
42
|
+
path = File.expand_path(ENV["WGPU_LIB_PATH"])
|
|
16
43
|
raise LoadError, "WGPU_LIB_PATH points to non-existent file: #{path}" unless File.exist?(path)
|
|
17
44
|
return path
|
|
18
45
|
end
|
|
19
46
|
|
|
20
|
-
cached_path = File.
|
|
21
|
-
return cached_path if
|
|
47
|
+
cached_path = Distribution.library_paths.find { |path| File.file?(path) }
|
|
48
|
+
return cached_path if cached_path
|
|
22
49
|
|
|
23
50
|
raise LoadError, <<~MSG
|
|
24
51
|
wgpu-native library not found.
|
|
25
|
-
|
|
52
|
+
Searched:
|
|
53
|
+
#{Distribution.library_paths.join("\n ")}
|
|
26
54
|
|
|
27
55
|
Try reinstalling the gem:
|
|
28
56
|
gem install wgpu
|
|
29
57
|
|
|
30
58
|
Or set WGPU_LIB_PATH environment variable to your custom wgpu-native build.
|
|
59
|
+
See docs/installation.md for manual installation instructions.
|
|
31
60
|
MSG
|
|
32
61
|
end
|
|
33
62
|
|
|
34
63
|
def cache_dir
|
|
35
|
-
|
|
64
|
+
Distribution.primary_cache_dir
|
|
36
65
|
end
|
|
37
66
|
|
|
38
67
|
def library_name
|
|
39
|
-
|
|
40
|
-
when /linux/ then "libwgpu_native.so"
|
|
41
|
-
when /darwin/ then "libwgpu_native.dylib"
|
|
42
|
-
when /mingw|mswin/ then "wgpu_native.dll"
|
|
43
|
-
else raise LoadError, "Unsupported OS: #{host_os}"
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
def host_os
|
|
50
|
-
RbConfig::CONFIG["host_os"]
|
|
68
|
+
Distribution.artifact_for.fetch(:library)
|
|
51
69
|
end
|
|
52
70
|
end
|
|
53
71
|
|
|
@@ -0,0 +1,171 @@
|
|
|
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
|
+
def register(resource)
|
|
17
|
+
return unless WGPU.debug_leaks
|
|
18
|
+
|
|
19
|
+
object_id = resource.object_id
|
|
20
|
+
description = describe(resource)
|
|
21
|
+
@mutex.synchronize { @resources[object_id] = description }
|
|
22
|
+
ObjectSpace.define_finalizer(resource, finalizer(object_id))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def unregister(resource)
|
|
26
|
+
@mutex.synchronize { @resources.delete(resource.object_id) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def warn_remaining
|
|
30
|
+
resources = @mutex.synchronize do
|
|
31
|
+
remaining = @resources.values
|
|
32
|
+
@resources.clear
|
|
33
|
+
remaining
|
|
34
|
+
end
|
|
35
|
+
resources.each { |description| warn "WGPU resource leaked: #{description}" }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def describe(resource)
|
|
39
|
+
label = resource.label
|
|
40
|
+
label_text = label ? " label=#{label.inspect}" : ""
|
|
41
|
+
"#{resource.class}#{label_text}"
|
|
42
|
+
end
|
|
43
|
+
private_class_method :describe
|
|
44
|
+
|
|
45
|
+
def finalizer(object_id)
|
|
46
|
+
proc do
|
|
47
|
+
description = @mutex.synchronize { @resources.delete(object_id) }
|
|
48
|
+
warn "WGPU resource leaked: #{description}" if description
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
private_class_method :finalizer
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
at_exit { LeakTracker.warn_remaining if WGPU.debug_leaks }
|
|
55
|
+
|
|
56
|
+
module CallbackKeepalive
|
|
57
|
+
INITIALIZATION_MUTEX = Mutex.new
|
|
58
|
+
|
|
59
|
+
module_function
|
|
60
|
+
|
|
61
|
+
def retain(owner, callback)
|
|
62
|
+
mutex, callbacks = storage_for(owner)
|
|
63
|
+
token = Object.new
|
|
64
|
+
mutex.synchronize { callbacks[token] = callback }
|
|
65
|
+
token
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def release(owner, token)
|
|
69
|
+
return unless token
|
|
70
|
+
|
|
71
|
+
mutex, callbacks = storage_for(owner)
|
|
72
|
+
mutex.synchronize { callbacks.delete(token) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def count(owner)
|
|
76
|
+
mutex, callbacks = storage_for(owner)
|
|
77
|
+
mutex.synchronize { callbacks.length }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def storage_for(owner)
|
|
81
|
+
INITIALIZATION_MUTEX.synchronize do
|
|
82
|
+
mutex = owner.instance_variable_get(:@wgpu_callback_keepalive_mutex)
|
|
83
|
+
callbacks = owner.instance_variable_get(:@wgpu_callback_keepalive)
|
|
84
|
+
unless mutex && callbacks
|
|
85
|
+
mutex = Mutex.new
|
|
86
|
+
callbacks = {}
|
|
87
|
+
owner.instance_variable_set(:@wgpu_callback_keepalive_mutex, mutex)
|
|
88
|
+
owner.instance_variable_set(:@wgpu_callback_keepalive, callbacks)
|
|
89
|
+
end
|
|
90
|
+
[mutex, callbacks]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
private_class_method :storage_for
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
module NativeResource
|
|
97
|
+
GUARDED_METHOD_EXEMPTIONS = [:initialize, :release, :released?, :handle, :label, :inspect].freeze
|
|
98
|
+
|
|
99
|
+
module Lifecycle
|
|
100
|
+
def initialize(*args, **kwargs, &block)
|
|
101
|
+
super
|
|
102
|
+
@released = false
|
|
103
|
+
@label = kwargs[:label] if kwargs.key?(:label)
|
|
104
|
+
LeakTracker.register(self)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def release(...)
|
|
108
|
+
return if released?
|
|
109
|
+
|
|
110
|
+
result = super
|
|
111
|
+
@released = true
|
|
112
|
+
LeakTracker.unregister(self)
|
|
113
|
+
result
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.included(base)
|
|
118
|
+
guard = Module.new
|
|
119
|
+
base.public_instance_methods(false).each do |method_name|
|
|
120
|
+
next if GUARDED_METHOD_EXEMPTIONS.include?(method_name)
|
|
121
|
+
|
|
122
|
+
guard.define_method(method_name) do |*args, **kwargs, &block|
|
|
123
|
+
ensure_not_released!
|
|
124
|
+
super(*args, **kwargs, &block)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
base.prepend(guard)
|
|
128
|
+
base.prepend(Lifecycle)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
attr_reader :label
|
|
132
|
+
|
|
133
|
+
def released?
|
|
134
|
+
return true if @released
|
|
135
|
+
return false unless instance_variable_defined?(:@handle)
|
|
136
|
+
return true if @handle.nil?
|
|
137
|
+
|
|
138
|
+
@handle.respond_to?(:null?) && @handle.null?
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Yields this wrapper and always releases it when the block exits.
|
|
142
|
+
#
|
|
143
|
+
# This is Ruby convenience API; WebGPU itself has no block-scoped resource
|
|
144
|
+
# primitive.
|
|
145
|
+
#
|
|
146
|
+
# @yieldparam resource [NativeResource] this resource
|
|
147
|
+
# @return the block result
|
|
148
|
+
def use
|
|
149
|
+
raise ArgumentError, "block is required" unless block_given?
|
|
150
|
+
|
|
151
|
+
ensure_not_released!
|
|
152
|
+
yield self
|
|
153
|
+
ensure
|
|
154
|
+
release if block_given? && !released?
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def inspect
|
|
158
|
+
label_text = @label ? " label=#{@label.inspect}" : ""
|
|
159
|
+
"#<#{self.class}#{label_text} released=#{released?}>"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
def ensure_not_released!
|
|
165
|
+
return unless released?
|
|
166
|
+
|
|
167
|
+
label_text = @label ? " (#{@label})" : ""
|
|
168
|
+
raise ResourceError, "#{self.class}#{label_text} has been released"
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -47,6 +47,18 @@ module WGPU
|
|
|
47
47
|
private
|
|
48
48
|
|
|
49
49
|
def create_entry(entry_hash)
|
|
50
|
+
DescriptorHelpers.validate_keys!(
|
|
51
|
+
entry_hash,
|
|
52
|
+
allowed: %i[binding buffer offset size sampler texture_view],
|
|
53
|
+
required: [:binding],
|
|
54
|
+
context: "bind group entry"
|
|
55
|
+
)
|
|
56
|
+
resources = %i[buffer sampler texture_view].select { |key| entry_hash[key] }
|
|
57
|
+
unless resources.one?
|
|
58
|
+
raise ArgumentError,
|
|
59
|
+
"bind group entry must define exactly one resource (:buffer, :sampler, or :texture_view)"
|
|
60
|
+
end
|
|
61
|
+
|
|
50
62
|
entry = Native::BindGroupEntry.new
|
|
51
63
|
entry[:next_in_chain] = nil
|
|
52
64
|
entry[:binding] = entry_hash[:binding]
|
|
@@ -13,30 +13,12 @@ module WGPU
|
|
|
13
13
|
|
|
14
14
|
def initialize(device, label: nil, entries:)
|
|
15
15
|
@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
|
|
16
|
+
desc, @descriptor_keepalive = build_descriptor(label:, entries:)
|
|
36
17
|
|
|
37
18
|
device.push_error_scope(:validation)
|
|
38
19
|
@handle = Native.wgpuDeviceCreateBindGroupLayout(device.handle, desc)
|
|
39
20
|
error = device.pop_error_scope
|
|
21
|
+
@descriptor_keepalive = nil
|
|
40
22
|
|
|
41
23
|
if @handle.null? || (error[:type] && error[:type] != :no_error)
|
|
42
24
|
msg = error[:message] || "Failed to create bind group layout"
|
|
@@ -52,7 +34,39 @@ module WGPU
|
|
|
52
34
|
|
|
53
35
|
private
|
|
54
36
|
|
|
37
|
+
def build_descriptor(label:, entries:)
|
|
38
|
+
keepalive = []
|
|
39
|
+
entries_array = entries.map { |entry| create_entry(entry) }
|
|
40
|
+
entries_ptr = FFI::MemoryPointer.new(Native::BindGroupLayoutEntry, entries_array.size)
|
|
41
|
+
entries_array.each_with_index do |entry, index|
|
|
42
|
+
offset = index * Native::BindGroupLayoutEntry.size
|
|
43
|
+
(entries_ptr + offset).put_bytes(0, entry.pointer.read_bytes(Native::BindGroupLayoutEntry.size))
|
|
44
|
+
end
|
|
45
|
+
keepalive.concat(entries_array)
|
|
46
|
+
keepalive << entries_ptr
|
|
47
|
+
|
|
48
|
+
desc = Native::BindGroupLayoutDescriptor.new
|
|
49
|
+
desc[:next_in_chain] = nil
|
|
50
|
+
DescriptorHelpers.set_label(desc, label, keepalive:)
|
|
51
|
+
desc[:entry_count] = entries_array.size
|
|
52
|
+
desc[:entries] = entries_ptr
|
|
53
|
+
[desc, keepalive]
|
|
54
|
+
end
|
|
55
|
+
|
|
55
56
|
def create_entry(entry_hash)
|
|
57
|
+
DescriptorHelpers.validate_keys!(
|
|
58
|
+
entry_hash,
|
|
59
|
+
allowed: %i[binding visibility buffer sampler texture storage_texture],
|
|
60
|
+
required: %i[binding visibility],
|
|
61
|
+
context: "bind group layout entry"
|
|
62
|
+
)
|
|
63
|
+
variants = %i[buffer sampler texture storage_texture].select { |key| entry_hash[key] }
|
|
64
|
+
unless variants.one?
|
|
65
|
+
raise ArgumentError,
|
|
66
|
+
"bind group layout entry must define exactly one resource variant " \
|
|
67
|
+
"(:buffer, :sampler, :texture, or :storage_texture)"
|
|
68
|
+
end
|
|
69
|
+
|
|
56
70
|
entry = Native::BindGroupLayoutEntry.new
|
|
57
71
|
entry[:next_in_chain] = nil
|
|
58
72
|
entry[:binding] = entry_hash[:binding]
|
|
@@ -78,44 +92,84 @@ module WGPU
|
|
|
78
92
|
|
|
79
93
|
if entry_hash[:buffer]
|
|
80
94
|
buffer_info = entry_hash[:buffer]
|
|
81
|
-
|
|
95
|
+
DescriptorHelpers.validate_keys!(
|
|
96
|
+
buffer_info,
|
|
97
|
+
allowed: %i[type has_dynamic_offset min_binding_size],
|
|
98
|
+
context: "buffer binding layout"
|
|
99
|
+
)
|
|
100
|
+
entry[:buffer][:type] = Native::EnumHelper.coerce(
|
|
101
|
+
Native::BufferBindingType,
|
|
102
|
+
buffer_info[:type] || :storage,
|
|
103
|
+
name: "buffer binding type"
|
|
104
|
+
)
|
|
82
105
|
entry[:buffer][:has_dynamic_offset] = buffer_info[:has_dynamic_offset] ? 1 : 0
|
|
83
106
|
entry[:buffer][:min_binding_size] = buffer_info[:min_binding_size] || 0
|
|
84
107
|
end
|
|
85
108
|
|
|
86
109
|
if entry_hash[:sampler]
|
|
87
110
|
sampler_info = entry_hash[:sampler]
|
|
88
|
-
|
|
111
|
+
DescriptorHelpers.validate_keys!(
|
|
112
|
+
sampler_info,
|
|
113
|
+
allowed: [:type],
|
|
114
|
+
context: "sampler binding layout"
|
|
115
|
+
)
|
|
116
|
+
entry[:sampler][:type] = Native::EnumHelper.coerce(
|
|
117
|
+
Native::SamplerBindingType,
|
|
118
|
+
sampler_info[:type] || :filtering,
|
|
119
|
+
name: "sampler binding type"
|
|
120
|
+
)
|
|
89
121
|
end
|
|
90
122
|
|
|
91
123
|
if entry_hash[:texture]
|
|
92
124
|
texture_info = entry_hash[:texture]
|
|
93
|
-
|
|
94
|
-
|
|
125
|
+
DescriptorHelpers.validate_keys!(
|
|
126
|
+
texture_info,
|
|
127
|
+
allowed: %i[sample_type view_dimension multisampled],
|
|
128
|
+
context: "texture binding layout"
|
|
129
|
+
)
|
|
130
|
+
entry[:texture][:sample_type] = Native::EnumHelper.coerce(
|
|
131
|
+
Native::TextureSampleType,
|
|
132
|
+
texture_info[:sample_type] || :float,
|
|
133
|
+
name: "texture sample type"
|
|
134
|
+
)
|
|
135
|
+
entry[:texture][:view_dimension] = Native::EnumHelper.coerce(
|
|
136
|
+
Native::TextureViewDimension,
|
|
137
|
+
texture_info[:view_dimension] || :d2,
|
|
138
|
+
name: "texture view dimension"
|
|
139
|
+
)
|
|
95
140
|
entry[:texture][:multisampled] = texture_info[:multisampled] ? 1 : 0
|
|
96
141
|
end
|
|
97
142
|
|
|
98
143
|
if entry_hash[:storage_texture]
|
|
99
144
|
st_info = entry_hash[:storage_texture]
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
145
|
+
DescriptorHelpers.validate_keys!(
|
|
146
|
+
st_info,
|
|
147
|
+
allowed: %i[access format view_dimension],
|
|
148
|
+
required: [:format],
|
|
149
|
+
context: "storage texture binding layout"
|
|
150
|
+
)
|
|
151
|
+
entry[:storage_texture][:access] = Native::EnumHelper.coerce(
|
|
152
|
+
Native::StorageTextureAccess,
|
|
153
|
+
st_info[:access] || :write_only,
|
|
154
|
+
name: "storage texture access"
|
|
155
|
+
)
|
|
156
|
+
entry[:storage_texture][:format] = Native::EnumHelper.coerce(
|
|
157
|
+
Native::TextureFormat,
|
|
158
|
+
st_info.fetch(:format),
|
|
159
|
+
name: "storage texture format"
|
|
160
|
+
)
|
|
161
|
+
entry[:storage_texture][:view_dimension] = Native::EnumHelper.coerce(
|
|
162
|
+
Native::TextureViewDimension,
|
|
163
|
+
st_info[:view_dimension] || :d2,
|
|
164
|
+
name: "texture view dimension"
|
|
165
|
+
)
|
|
103
166
|
end
|
|
104
167
|
|
|
105
168
|
entry
|
|
106
169
|
end
|
|
107
170
|
|
|
108
171
|
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
|
|
172
|
+
Native::EnumHelper.coerce_flags(Native::ShaderStage, visibility, name: "shader visibility")
|
|
119
173
|
end
|
|
120
174
|
end
|
|
121
175
|
end
|