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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +88 -0
  3. data/README.md +44 -5
  4. data/docs/README.md +22 -0
  5. data/docs/api_coverage.md +140 -0
  6. data/docs/async.md +41 -0
  7. data/docs/bind_groups.md +25 -0
  8. data/docs/buffer_data.md +37 -0
  9. data/docs/command_encoding.md +49 -0
  10. data/docs/errors.md +40 -0
  11. data/docs/getting_started_compute.md +62 -0
  12. data/docs/getting_started_rendering.md +62 -0
  13. data/docs/installation.md +94 -0
  14. data/docs/pipeline_descriptors.md +43 -0
  15. data/docs/releasing.md +32 -0
  16. data/docs/resource_lifetime.md +108 -0
  17. data/docs/shaders.md +32 -0
  18. data/docs/texture_readback.md +46 -0
  19. data/docs/troubleshooting.md +53 -0
  20. data/docs/upgrading_wgpu_native.md +37 -0
  21. data/ext/wgpu/extconf.rb +10 -142
  22. data/lib/wgpu/async_task.rb +19 -0
  23. data/lib/wgpu/commands/command_buffer.rb +25 -1
  24. data/lib/wgpu/commands/command_encoder.rb +123 -9
  25. data/lib/wgpu/commands/compute_pass.rb +53 -0
  26. data/lib/wgpu/commands/render_bundle.rb +9 -1
  27. data/lib/wgpu/commands/render_bundle_encoder.rb +65 -4
  28. data/lib/wgpu/commands/render_pass.rb +136 -8
  29. data/lib/wgpu/core/adapter.rb +123 -19
  30. data/lib/wgpu/core/async_waiter.rb +47 -4
  31. data/lib/wgpu/core/canvas_context.rb +32 -2
  32. data/lib/wgpu/core/device.rb +399 -53
  33. data/lib/wgpu/core/instance.rb +28 -4
  34. data/lib/wgpu/core/queue.rb +197 -51
  35. data/lib/wgpu/core/surface.rb +64 -17
  36. data/lib/wgpu/data_types.rb +83 -0
  37. data/lib/wgpu/descriptor_helpers.rb +104 -0
  38. data/lib/wgpu/error.rb +70 -0
  39. data/lib/wgpu/logging.rb +63 -0
  40. data/lib/wgpu/native/abi_verifier.rb +143 -0
  41. data/lib/wgpu/native/callbacks.rb +10 -1
  42. data/lib/wgpu/native/capabilities.rb +32 -2
  43. data/lib/wgpu/native/distribution.rb +176 -0
  44. data/lib/wgpu/native/enum_helper.rb +80 -0
  45. data/lib/wgpu/native/enums.rb +68 -8
  46. data/lib/wgpu/native/fixtures/webgpu-v27.0.4.0-enums.h +848 -0
  47. data/lib/wgpu/native/functions.rb +21 -10
  48. data/lib/wgpu/native/installer.rb +223 -0
  49. data/lib/wgpu/native/loader.rb +61 -21
  50. data/lib/wgpu/native/structs.rb +18 -1
  51. data/lib/wgpu/native_resource.rb +342 -0
  52. data/lib/wgpu/pipeline/bind_group.rb +21 -0
  53. data/lib/wgpu/pipeline/bind_group_layout.rb +108 -41
  54. data/lib/wgpu/pipeline/compute_pipeline.rb +40 -50
  55. data/lib/wgpu/pipeline/pipeline_layout.rb +8 -0
  56. data/lib/wgpu/pipeline/render_pipeline.rb +207 -110
  57. data/lib/wgpu/pipeline/shader_module.rb +95 -28
  58. data/lib/wgpu/resources/buffer.rb +387 -85
  59. data/lib/wgpu/resources/query_set.rb +26 -12
  60. data/lib/wgpu/resources/sampler.rb +43 -20
  61. data/lib/wgpu/resources/texture.rb +89 -48
  62. data/lib/wgpu/resources/texture_view.rb +35 -7
  63. data/lib/wgpu/texture_format.rb +96 -0
  64. data/lib/wgpu/version.rb +1 -1
  65. data/lib/wgpu/window.rb +34 -1
  66. data/lib/wgpu.rb +32 -0
  67. data/sig/wgpu.rbs +460 -0
  68. metadata +33 -16
@@ -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
 
@@ -17,11 +20,8 @@ module WGPU
17
20
  attach_function :wgpuInstanceProcessEvents,
18
21
  [:pointer], :void
19
22
 
20
- begin
21
- attach_function :wgpuInstanceWaitAny,
22
- [:pointer, :size_t, :pointer, :uint64], WaitStatus
23
- rescue FFI::NotFoundError
24
- end
23
+ attach_optional_function :wgpuInstanceWaitAny,
24
+ [:pointer, :size_t, :pointer, :uint64], WaitStatus
25
25
 
26
26
  attach_function :wgpuAdapterRelease,
27
27
  [:pointer], :void
@@ -71,17 +71,22 @@ module WGPU
71
71
  attach_function :wgpuDeviceCreateRenderPipeline,
72
72
  [:pointer, :pointer], :pointer
73
73
 
74
+ attach_function :wgpuDeviceCreateRenderPipelineAsync,
75
+ [:pointer, RenderPipelineDescriptor.by_ref, CreateRenderPipelineAsyncCallbackInfo.by_value],
76
+ Future.by_value
77
+
74
78
  attach_function :wgpuDeviceCreateComputePipeline,
75
79
  [:pointer, ComputePipelineDescriptor.by_ref], :pointer
76
80
 
81
+ attach_function :wgpuDeviceCreateComputePipelineAsync,
82
+ [:pointer, ComputePipelineDescriptor.by_ref, CreateComputePipelineAsyncCallbackInfo.by_value],
83
+ Future.by_value
84
+
77
85
  attach_function :wgpuDeviceCreateCommandEncoder,
78
86
  [:pointer, CommandEncoderDescriptor.by_ref], :pointer
79
87
 
80
- begin
81
- attach_function :wgpuDevicePoll,
82
- [:pointer, :uint32, :pointer], :uint32
83
- rescue FFI::NotFoundError
84
- end
88
+ attach_optional_function :wgpuDevicePoll,
89
+ [:pointer, :uint32, :pointer], :uint32
85
90
 
86
91
  attach_function :wgpuQueueRelease,
87
92
  [:pointer], :void
@@ -424,5 +429,11 @@ module WGPU
424
429
 
425
430
  attach_function :wgpuRenderBundleEncoderInsertDebugMarker,
426
431
  [:pointer, StringView.by_value], :void
432
+
433
+ attach_optional_function :wgpuSetLogCallback,
434
+ [:log_callback, :pointer], :void
435
+
436
+ attach_optional_function :wgpuSetLogLevel,
437
+ [LogLevel], :void
427
438
  end
428
439
  end
@@ -0,0 +1,223 @@
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
+ # Downloads, verifies, and installs the pinned wgpu-native artifact.
15
+ class Installer
16
+ attr_reader :platform
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
32
+ def initialize(platform: RUBY_PLATFORM, env: ENV, home: Dir.home,
33
+ host_os: RbConfig::CONFIG["host_os"], output: $stdout,
34
+ command_runner: nil, http_factory: nil, zip_file_loader: nil,
35
+ distribution: Distribution)
36
+ @platform = platform
37
+ @env = env
38
+ @home = home
39
+ @host_os = host_os
40
+ @output = output
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
54
+ def install
55
+ return custom_library_path if custom_library_path
56
+
57
+ artifact = @distribution.artifact_for(platform)
58
+ cached_path = existing_library_path(artifact)
59
+ if cached_path
60
+ @output.puts "wgpu-native already cached at #{cached_path}"
61
+ return cached_path
62
+ end
63
+
64
+ install_artifact(artifact)
65
+ rescue LoadError, InstallError => error
66
+ raise InstallError, "#{error.message}\n\n#{recovery_instructions}"
67
+ end
68
+
69
+ # Returns the primary versioned cache directory.
70
+ #
71
+ # @return [String]
72
+ def clean_path
73
+ @distribution.primary_cache_dir(env: @env, home: @home, host_os: @host_os)
74
+ end
75
+
76
+ private
77
+
78
+ def custom_library_path
79
+ path = @env["WGPU_LIB_PATH"]
80
+ return if path.nil? || path.empty?
81
+ raise InstallError, "WGPU_LIB_PATH points to a non-existent file: #{path}" unless File.file?(path)
82
+
83
+ File.expand_path(path)
84
+ end
85
+
86
+ def existing_library_path(artifact)
87
+ @distribution.cache_directories(env: @env, home: @home, host_os: @host_os).each do |directory|
88
+ path = File.join(directory, "lib", artifact.fetch(:library))
89
+ return path if File.file?(path)
90
+ end
91
+ nil
92
+ end
93
+
94
+ def install_artifact(artifact)
95
+ cache_dir = clean_path
96
+ archive_path = File.join(cache_dir, artifact.fetch(:archive))
97
+ library_path = File.join(cache_dir, "lib", artifact.fetch(:library))
98
+ FileUtils.mkdir_p(cache_dir)
99
+
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)
103
+ verify_checksum!(archive_path, artifact.fetch(:sha256))
104
+
105
+ @output.puts "Extracting to #{cache_dir}..."
106
+ extract_zip(archive_path, cache_dir)
107
+ raise InstallError, "Archive did not contain lib/#{artifact.fetch(:library)}" unless File.file?(library_path)
108
+
109
+ @output.puts "wgpu-native installed successfully!"
110
+ library_path
111
+ ensure
112
+ FileUtils.rm_f(archive_path) if archive_path
113
+ end
114
+
115
+ def verify_checksum!(path, expected)
116
+ actual = Digest::SHA256.file(path).hexdigest
117
+ return if actual == expected
118
+
119
+ FileUtils.rm_f(path)
120
+ raise InstallError, "SHA-256 mismatch for #{File.basename(path)} (expected #{expected}, got #{actual})"
121
+ end
122
+
123
+ def download_file(url, destination)
124
+ return if curl_available? && download_with_curl(url, destination)
125
+ return if download_with_ruby(url, destination)
126
+
127
+ raise InstallError, "Download failed: #{url}"
128
+ end
129
+
130
+ def curl_available?
131
+ @command_runner.call("curl", "--version", out: File::NULL, err: File::NULL)
132
+ end
133
+
134
+ def download_with_curl(url, destination)
135
+ @command_runner.call("curl", "-fsSL", "-o", destination, url)
136
+ end
137
+
138
+ def download_with_ruby(url, destination, redirects = 5)
139
+ raise InstallError, "Too many redirects while downloading #{url}" if redirects.zero?
140
+
141
+ uri = URI.parse(url)
142
+ http = @http_factory.call(uri)
143
+ http.use_ssl = uri.scheme == "https"
144
+ http.open_timeout = 10
145
+ http.read_timeout = 120
146
+
147
+ http.request(Net::HTTP::Get.new(uri.request_uri)) do |response|
148
+ case response
149
+ when Net::HTTPRedirection
150
+ location = response["location"]
151
+ raise InstallError, "Redirect response did not include a location" unless location
152
+
153
+ location = URI.join(url, location).to_s
154
+ return download_with_ruby(location, destination, redirects - 1)
155
+ when Net::HTTPSuccess
156
+ File.open(destination, "wb") do |file|
157
+ response.read_body { |chunk| file.write(chunk) }
158
+ end
159
+ return true
160
+ end
161
+ end
162
+
163
+ false
164
+ rescue SystemCallError, SocketError, URI::InvalidURIError => error
165
+ @output.puts "Ruby download failed: #{error.message}"
166
+ false
167
+ end
168
+
169
+ def extract_zip(archive_path, destination)
170
+ return if extract_with_rubyzip(archive_path, destination)
171
+ return if windows? && extract_with_powershell(archive_path, destination)
172
+ return if extract_with_unzip(archive_path, destination)
173
+
174
+ raise InstallError, "Failed to extract zip; install the rubyzip gem or a supported system extractor"
175
+ end
176
+
177
+ def extract_with_rubyzip(archive_path, destination)
178
+ zip_file_class = @zip_file_loader.call
179
+ destination_root = "#{File.expand_path(destination)}#{File::SEPARATOR}"
180
+
181
+ zip_file_class.open(archive_path) do |zip_file|
182
+ zip_file.each do |entry|
183
+ target = File.expand_path(entry.name, destination)
184
+ unless target.start_with?(destination_root)
185
+ raise InstallError, "Archive entry escapes destination: #{entry.name}"
186
+ end
187
+
188
+ FileUtils.mkdir_p(entry.directory? ? target : File.dirname(target))
189
+ entry.extract(target) { true } unless entry.directory?
190
+ end
191
+ end
192
+ true
193
+ rescue LoadError
194
+ false
195
+ end
196
+
197
+ def extract_with_powershell(archive_path, destination)
198
+ @command_runner.call(
199
+ "powershell",
200
+ "-NoProfile",
201
+ "-Command",
202
+ "Expand-Archive -Force -LiteralPath '#{archive_path}' -DestinationPath '#{destination}'"
203
+ )
204
+ end
205
+
206
+ def extract_with_unzip(archive_path, destination)
207
+ @command_runner.call("unzip", "-o", "-q", archive_path, "-d", destination)
208
+ end
209
+
210
+ def windows?
211
+ /mingw|mswin/.match?(platform)
212
+ end
213
+
214
+ def recovery_instructions
215
+ <<~INSTRUCTIONS.chomp
216
+ Download the matching #{@distribution.const_get(:VERSION)} artifact manually and set:
217
+ WGPU_LIB_PATH=/absolute/path/to/the/wgpu-native/shared-library
218
+ See docs/installation.md for platform details.
219
+ INSTRUCTIONS
220
+ end
221
+ end
222
+ end
223
+ end
@@ -1,53 +1,93 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "ffi"
4
- require "rbconfig"
4
+ require_relative "distribution"
5
5
 
6
6
  module WGPU
7
7
  module Native
8
- extend FFI::Library
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]
15
+ def attach_optional_function(name, arguments, result)
16
+ attach_function(name, arguments, result)
17
+ optional_functions[name] = true
18
+ rescue FFI::NotFoundError
19
+ optional_functions[name] = false
20
+ define_singleton_method(name) do |*|
21
+ raise WGPU::Error,
22
+ "Optional wgpu-native function #{name} is unavailable in the loaded library " \
23
+ "(expected #{Distribution::VERSION})"
24
+ end
25
+ end
26
+
27
+ # Reports whether an optional native function was attached.
28
+ # @param name [Symbol] exported function name
29
+ # @return [Boolean]
30
+ def optional_function_available?(name)
31
+ optional_functions.fetch(name, false)
32
+ end
33
+
34
+ # Returns availability information for optional native functions.
35
+ #
36
+ # @return [Hash{Symbol => Boolean}] immutable capability snapshot
37
+ def optional_capabilities
38
+ optional_functions.dup.freeze
39
+ end
40
+
41
+ private
9
42
 
10
- WGPU_VERSION = "v27.0.4.0"
43
+ def optional_functions
44
+ @optional_functions ||= {}
45
+ end
46
+ end
47
+
48
+ extend FFI::Library
49
+ extend OptionalFunctions
11
50
 
12
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
13
55
  def library_path
14
- if ENV["WGPU_LIB_PATH"]
15
- path = ENV["WGPU_LIB_PATH"]
56
+ if ENV["WGPU_LIB_PATH"] && !ENV["WGPU_LIB_PATH"].empty?
57
+ path = File.expand_path(ENV["WGPU_LIB_PATH"])
16
58
  raise LoadError, "WGPU_LIB_PATH points to non-existent file: #{path}" unless File.exist?(path)
17
59
  return path
18
60
  end
19
61
 
20
- cached_path = File.join(cache_dir, "lib", library_name)
21
- return cached_path if File.exist?(cached_path)
62
+ cached_path = Distribution.library_paths.find { |path| File.file?(path) }
63
+ return cached_path if cached_path
22
64
 
23
65
  raise LoadError, <<~MSG
24
66
  wgpu-native library not found.
25
- Expected at: #{cached_path}
67
+ Searched:
68
+ #{Distribution.library_paths.join("\n ")}
26
69
 
27
70
  Try reinstalling the gem:
28
71
  gem install wgpu
29
72
 
30
73
  Or set WGPU_LIB_PATH environment variable to your custom wgpu-native build.
74
+ See docs/installation.md for manual installation instructions.
31
75
  MSG
32
76
  end
33
77
 
78
+ # Returns the preferred native-library cache directory.
79
+ #
80
+ # @return [String] absolute cache directory
34
81
  def cache_dir
35
- File.join(Dir.home, ".cache", "wgpu-ruby", WGPU_VERSION)
82
+ Distribution.primary_cache_dir
36
83
  end
37
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
38
89
  def library_name
39
- case host_os
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"]
90
+ Distribution.artifact_for.fetch(:library)
51
91
  end
52
92
  end
53
93
 
@@ -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