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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +66 -0
  3. data/README.md +18 -2
  4. data/docs/README.md +2 -2
  5. data/docs/api_coverage.md +23 -15
  6. data/docs/async.md +13 -1
  7. data/docs/command_encoding.md +25 -0
  8. data/docs/errors.md +13 -0
  9. data/docs/getting_started_compute.md +1 -0
  10. data/docs/getting_started_rendering.md +5 -0
  11. data/docs/pipeline_descriptors.md +7 -2
  12. data/docs/releasing.md +11 -1
  13. data/docs/resource_lifetime.md +25 -2
  14. data/docs/texture_readback.md +27 -0
  15. data/docs/troubleshooting.md +5 -0
  16. data/docs/upgrading_wgpu_native.md +16 -5
  17. data/lib/wgpu/async_task.rb +19 -0
  18. data/lib/wgpu/commands/command_buffer.rb +14 -1
  19. data/lib/wgpu/commands/command_encoder.rb +82 -27
  20. data/lib/wgpu/commands/compute_pass.rb +49 -6
  21. data/lib/wgpu/commands/render_bundle.rb +9 -1
  22. data/lib/wgpu/commands/render_bundle_encoder.rb +63 -9
  23. data/lib/wgpu/commands/render_pass.rb +116 -14
  24. data/lib/wgpu/core/adapter.rb +95 -13
  25. data/lib/wgpu/core/async_waiter.rb +30 -4
  26. data/lib/wgpu/core/canvas_context.rb +37 -2
  27. data/lib/wgpu/core/device.rb +377 -70
  28. data/lib/wgpu/core/instance.rb +20 -0
  29. data/lib/wgpu/core/queue.rb +143 -43
  30. data/lib/wgpu/core/surface.rb +55 -7
  31. data/lib/wgpu/data_types.rb +16 -0
  32. data/lib/wgpu/descriptor_helpers.rb +87 -0
  33. data/lib/wgpu/error.rb +15 -0
  34. data/lib/wgpu/native/abi_verifier.rb +37 -3
  35. data/lib/wgpu/native/callbacks.rb +6 -0
  36. data/lib/wgpu/native/capabilities.rb +16 -2
  37. data/lib/wgpu/native/distribution.rb +63 -0
  38. data/lib/wgpu/native/enum_helper.rb +17 -0
  39. data/lib/wgpu/native/enums.rb +8 -0
  40. data/lib/wgpu/native/fixtures/webgpu-v27.0.4.0-enums.h +848 -0
  41. data/lib/wgpu/native/functions.rb +14 -0
  42. data/lib/wgpu/native/installer.rb +55 -18
  43. data/lib/wgpu/native/loader.rb +22 -0
  44. data/lib/wgpu/native/structs.rb +18 -1
  45. data/lib/wgpu/native_resource.rb +190 -3
  46. data/lib/wgpu/pipeline/bind_group.rb +16 -7
  47. data/lib/wgpu/pipeline/bind_group_layout.rb +20 -7
  48. data/lib/wgpu/pipeline/compute_pipeline.rb +25 -37
  49. data/lib/wgpu/pipeline/pipeline_layout.rb +12 -4
  50. data/lib/wgpu/pipeline/render_pipeline.rb +33 -48
  51. data/lib/wgpu/pipeline/shader_module.rb +57 -32
  52. data/lib/wgpu/resources/buffer.rb +330 -59
  53. data/lib/wgpu/resources/query_set.rb +13 -1
  54. data/lib/wgpu/resources/sampler.rb +10 -3
  55. data/lib/wgpu/resources/texture.rb +49 -12
  56. data/lib/wgpu/resources/texture_view.rb +25 -5
  57. data/lib/wgpu/texture_format.rb +14 -0
  58. data/lib/wgpu/version.rb +1 -1
  59. data/lib/wgpu/window.rb +26 -0
  60. data/sig/wgpu.rbs +85 -5
  61. metadata +12 -4
@@ -2,8 +2,38 @@
2
2
 
3
3
  module WGPU
4
4
  module DescriptorHelpers
5
+ SIZE_MAX = (1 << (FFI.type_size(:size_t) * 8)) - 1
6
+
5
7
  module_function
6
8
 
9
+ # Normalizes an Array or Hash texture extent to a native descriptor.
10
+ # @param size [Array<Integer>, Hash] width, height, and depth/layer count
11
+ # @return [Native::Extent3D]
12
+ def extent_3d(size)
13
+ values = case size
14
+ when Array
15
+ [size.fetch(0), size[1] || 1, size[2] || 1]
16
+ when Hash
17
+ [size.fetch(:width), size[:height] || 1, size[:depth_or_array_layers] || 1]
18
+ else
19
+ raise ArgumentError, "texture size must be an Array or Hash"
20
+ end
21
+ extent = Native::Extent3D.new
22
+ [:width, :height, :depth_or_array_layers].zip(values).each do |field, value|
23
+ value = Integer(value)
24
+ raise ArgumentError, "#{field} must be between 0 and 4294967295" unless (0..0xFFFFFFFF).cover?(value)
25
+
26
+ extent[field] = value
27
+ end
28
+ extent
29
+ end
30
+
31
+ # Writes an optional Ruby label into a native descriptor.
32
+ #
33
+ # @param descriptor [FFI::Struct] descriptor with a +label+ member
34
+ # @param label [String, nil] label text
35
+ # @param keepalive [Array] receives allocated pointers that must remain alive
36
+ # @return [void]
7
37
  def set_label(descriptor, label, keepalive:)
8
38
  if label
9
39
  pointer = FFI::MemoryPointer.from_string(label)
@@ -16,6 +46,11 @@ module WGPU
16
46
  end
17
47
  end
18
48
 
49
+ # Allocates and fills a native uint32 array.
50
+ #
51
+ # @param values [Array<Integer>] values to copy
52
+ # @param keepalive [Array] receives the allocated pointer
53
+ # @return [FFI::MemoryPointer, nil] pointer, or +nil+ for an empty array
19
54
  def uint32_array(values, keepalive:)
20
55
  return nil if values.empty?
21
56
 
@@ -25,6 +60,58 @@ module WGPU
25
60
  pointer
26
61
  end
27
62
 
63
+ # Writes a nullable string into a native string view.
64
+ #
65
+ # @param string_view [Native::StringView] destination view
66
+ # @param value [String, nil] string value; +nil+ uses the native null sentinel
67
+ # @param keepalive [Array] receives allocated pointers that must remain alive
68
+ # @return [void]
69
+ def set_nullable_string_view(string_view, value, keepalive:)
70
+ if value.nil?
71
+ string_view[:data] = nil
72
+ string_view[:length] = SIZE_MAX
73
+ return
74
+ end
75
+
76
+ string = value
77
+ pointer = FFI::MemoryPointer.from_string(string)
78
+ keepalive << pointer
79
+ string_view[:data] = pointer
80
+ string_view[:length] = string.bytesize
81
+ end
82
+
83
+ # Writes pipeline-overridable constants into a stage descriptor.
84
+ #
85
+ # @param stage_descriptor [FFI::Struct] programmable stage descriptor
86
+ # @param constants [Hash{#to_s => Numeric}, nil] constant names and values
87
+ # @param keepalive [Array] receives allocated pointers that must remain alive
88
+ # @return [void]
89
+ def set_constants(stage_descriptor, constants, keepalive:)
90
+ stage_descriptor[:constant_count] = 0
91
+ stage_descriptor[:constants] = nil
92
+ return if constants.nil? || constants.empty?
93
+
94
+ constants_pointer = FFI::MemoryPointer.new(Native::ConstantEntry, constants.size)
95
+ keepalive << constants_pointer
96
+
97
+ constants.each_with_index do |(key, value), index|
98
+ entry_pointer = constants_pointer + (index * Native::ConstantEntry.size)
99
+ entry = Native::ConstantEntry.new(entry_pointer)
100
+ entry[:next_in_chain] = nil
101
+ set_nullable_string_view(entry[:key], key.to_s, keepalive:)
102
+ entry[:value] = value.to_f
103
+ end
104
+
105
+ stage_descriptor[:constant_count] = constants.size
106
+ stage_descriptor[:constants] = constants_pointer
107
+ end
108
+
109
+ # Checks required keys and warns about unsupported descriptor keys.
110
+ # @param options [Hash] descriptor options
111
+ # @param allowed [Array<Symbol>] supported keys
112
+ # @param required [Array<Symbol>] mandatory keys
113
+ # @return [Hash] original options
114
+ # @raise [ArgumentError] when required keys are missing
28
115
  def validate_keys!(options, allowed:, required: [], context: "descriptor")
29
116
  return options unless options.is_a?(Hash)
30
117
 
data/lib/wgpu/error.rb CHANGED
@@ -20,6 +20,9 @@ module WGPU
20
20
  class SurfaceAcquisitionError < SurfaceError
21
21
  attr_reader :status
22
22
 
23
+ # Creates an acquisition error for a native surface status.
24
+ # @param status [Symbol] native acquisition status
25
+ # @param message [String, nil] optional override message
23
26
  def initialize(status, message = nil)
24
27
  @status = status
25
28
  super(message || "Failed to get current surface texture: #{status}")
@@ -28,12 +31,17 @@ module WGPU
28
31
  class RenderBundleError < Error; end
29
32
 
30
33
  GPUError = Data.define(:type, :message) do
34
+ # Converts a native error-scope result into a typed error.
35
+ # @param error [Hash, nil] native error result
36
+ # @return [GPUError, nil]
31
37
  def self.from_hash(error)
32
38
  return if error.nil? || error[:type].nil? || error[:type] == :no_error
33
39
 
34
40
  new(type: error[:type], message: error[:message].to_s)
35
41
  end
36
42
 
43
+ # Returns the Ruby exception class matching this GPU error type.
44
+ # @return [Class<Error>]
37
45
  def exception_class
38
46
  {
39
47
  validation: ValidationError,
@@ -43,10 +51,15 @@ module WGPU
43
51
  }.fetch(type, Error)
44
52
  end
45
53
 
54
+ # Raises this error as its matching Ruby exception.
55
+ # @return [void]
56
+ # @raise [Error]
46
57
  def raise!
47
58
  raise exception_class, "GPU error (#{type}): #{message}"
48
59
  end
49
60
 
61
+ # Returns a serializable representation of the error.
62
+ # @return [Hash]
50
63
  def to_h
51
64
  { type:, message: }
52
65
  end
@@ -63,6 +76,8 @@ module WGPU
63
76
  alias line line_num
64
77
  alias column line_pos
65
78
 
79
+ # Formats the diagnostic with source location, severity, and message.
80
+ # @return [String]
66
81
  def to_s
67
82
  location = line_num&.positive? ? "#{line_num}:#{line_pos}" : "unknown location"
68
83
  "#{location}: #{type}: #{message}"
@@ -22,18 +22,29 @@ module WGPU
22
22
  ]
23
23
  }.freeze
24
24
 
25
+ # Creates a verifier for a header and native binding.
26
+ # @param header_path [String, nil] header fixture to compare
27
+ # @param native [Module] native binding module
25
28
  def initialize(header_path: nil, native: Native)
26
29
  @header_path = header_path || self.class.default_header_path
27
30
  @native = native
28
31
  end
29
32
 
33
+ # Verifies the runtime version and Ruby enum ABI against the header.
34
+ # @return [true]
35
+ # @raise [WGPU::Error] when differences are detected
30
36
  def verify!
31
37
  differences = enum_differences
38
+ version_difference = native_version_difference
39
+ differences.unshift(version_difference) if version_difference
32
40
  return true if differences.empty?
33
41
 
34
- raise WGPU::Error, "wgpu-native ABI enum differences:\n#{differences.join("\n")}"
42
+ raise WGPU::Error, "wgpu-native ABI differences:\n#{differences.join("\n")}"
35
43
  end
36
44
 
45
+ # Compares Ruby enum values with the pinned native header.
46
+ #
47
+ # @return [Array<String>] human-readable differences
37
48
  def enum_differences
38
49
  header_enums = parse_header
39
50
  ruby_enums.filter_map do |name, mapping|
@@ -44,10 +55,16 @@ module WGPU
44
55
  end
45
56
  end
46
57
 
58
+ # Locates the pinned header used for ABI verification.
59
+ # @return [String] absolute header path
60
+ # @raise [WGPU::Error] if no header is available
47
61
  def self.default_header_path
48
62
  override = ENV["WGPU_HEADER_PATH"]
49
63
  return File.expand_path(override) if override && !override.empty?
50
64
 
65
+ fixture = fixture_header_path
66
+ return fixture if File.file?(fixture)
67
+
51
68
  candidates = Distribution.cache_directories.map do |cache_dir|
52
69
  File.join(cache_dir, "include", "webgpu", "webgpu.h")
53
70
  end
@@ -56,13 +73,30 @@ module WGPU
56
73
 
57
74
  raise WGPU::Error, <<~MSG.chomp
58
75
  Pinned #{Distribution::VERSION} webgpu.h was not found.
59
- Run `bundle exec rake wgpu:install`, or set WGPU_HEADER_PATH.
60
- Searched: #{candidates.join(", ")}
76
+ Restore the repository ABI fixture, run `bundle exec rake wgpu:install`,
77
+ or set WGPU_HEADER_PATH.
78
+ Searched: #{([fixture] + candidates).join(", ")}
61
79
  MSG
62
80
  end
63
81
 
82
+ # Returns the repository's pinned enum fixture path.
83
+ #
84
+ # @return [String] absolute header fixture path
85
+ def self.fixture_header_path
86
+ File.expand_path("fixtures/webgpu-#{Distribution::VERSION}-enums.h", __dir__)
87
+ end
88
+
64
89
  private
65
90
 
91
+ def native_version_difference
92
+ actual = @native.wgpuGetVersion
93
+ expected = Distribution::ENCODED_VERSION
94
+ return if actual == expected
95
+
96
+ "version: runtime #{Distribution.version_string(actual)} (0x#{format("%08x", actual)}) " \
97
+ "does not match pinned #{Distribution::VERSION} (0x#{format("%08x", expected)})"
98
+ end
99
+
66
100
  def parse_header
67
101
  source = File.read(@header_path)
68
102
  source.scan(
@@ -8,6 +8,12 @@ module WGPU
8
8
  callback :request_device_callback,
9
9
  [RequestDeviceStatus, :pointer, StringView.by_value, :pointer, :pointer], :void
10
10
 
11
+ callback :create_compute_pipeline_async_callback,
12
+ [CreatePipelineAsyncStatus, :pointer, StringView.by_value, :pointer, :pointer], :void
13
+
14
+ callback :create_render_pipeline_async_callback,
15
+ [CreatePipelineAsyncStatus, :pointer, StringView.by_value, :pointer, :pointer], :void
16
+
11
17
  callback :buffer_map_callback,
12
18
  [MapAsyncStatus, StringView.by_value, :pointer, :pointer], :void
13
19
 
@@ -3,10 +3,14 @@
3
3
  module WGPU
4
4
  module Native
5
5
  class << self
6
+ # Reports whether future-based callback waiting is available.
7
+ # @return [Boolean]
6
8
  def future_api?
7
9
  optional_function_available?(:wgpuInstanceWaitAny)
8
10
  end
9
11
 
12
+ # Reports whether explicit device polling is available.
13
+ # @return [Boolean]
10
14
  def device_poll_available?
11
15
  optional_function_available?(:wgpuDevicePoll)
12
16
  end
@@ -15,13 +19,23 @@ module WGPU
15
19
  # implementation is a Rust panic stub. Calling it aborts the process, so
16
20
  # symbol presence alone cannot be used as a capability check.
17
21
  def compilation_info_available?
18
- Distribution::VERSION != "v27.0.4.0"
22
+ Distribution.capability_implemented?(:compilation_info)
19
23
  end
20
24
 
25
+ # Reports whether querying buffer map state is safe in the pinned runtime.
26
+ # @return [Boolean]
21
27
  def buffer_map_state_available?
22
- Distribution::VERSION != "v27.0.4.0"
28
+ Distribution.capability_implemented?(:buffer_map_state)
23
29
  end
24
30
 
31
+ # Reports whether native asynchronous pipeline creation is implemented.
32
+ # @return [Boolean]
33
+ def pipeline_async_available?
34
+ Distribution.capability_implemented?(:pipeline_async)
35
+ end
36
+
37
+ # Reports whether native logging callbacks and levels are available.
38
+ # @return [Boolean]
25
39
  def logging_available?
26
40
  optional_function_available?(:wgpuSetLogCallback) &&
27
41
  optional_function_available?(:wgpuSetLogLevel)
@@ -7,6 +7,17 @@ module WGPU
7
7
  module Distribution
8
8
  VERSION = "v27.0.4.0"
9
9
  RELEASE_BASE_URL = "https://github.com/gfx-rs/wgpu-native/releases/download/#{VERSION}"
10
+ VERSION_COMPONENTS = VERSION.delete_prefix("v").split(".").map { |part| Integer(part, 10) }.freeze
11
+ unless VERSION_COMPONENTS.length == 4 && VERSION_COMPONENTS.all? { |part| part.between?(0, 255) }
12
+ raise "wgpu-native VERSION must have four byte-sized components: #{VERSION}"
13
+ end
14
+ # wgpuGetVersion packs vMAJOR.MINOR.PATCH.BUILD into four big-endian bytes.
15
+ ENCODED_VERSION = VERSION_COMPONENTS.reduce(0) { |encoded, part| (encoded << 8) | part }
16
+ UNIMPLEMENTED_CAPABILITIES = {
17
+ compilation_info: [VERSION].freeze,
18
+ buffer_map_state: [VERSION].freeze,
19
+ pipeline_async: [VERSION].freeze
20
+ }.freeze
10
21
 
11
22
  ARTIFACTS = [
12
23
  {
@@ -43,6 +54,10 @@ module WGPU
43
54
 
44
55
  module_function
45
56
 
57
+ # Selects the native release artifact for a Ruby platform.
58
+ # @param platform [String] Ruby platform identifier
59
+ # @return [Hash] artifact metadata
60
+ # @raise [LoadError] if the platform is unsupported
46
61
  def artifact_for(platform = RUBY_PLATFORM)
47
62
  raise LoadError, unsupported_platform_message(platform) if platform.include?("musl")
48
63
 
@@ -52,6 +67,12 @@ module WGPU
52
67
  raise LoadError, unsupported_platform_message(platform)
53
68
  end
54
69
 
70
+ # Returns the preferred versioned native-library cache directory.
71
+ #
72
+ # @param env [Hash] environment used for cache overrides
73
+ # @param home [String] user home directory
74
+ # @param host_os [String] host operating-system identifier
75
+ # @return [String] absolute cache directory
55
76
  def primary_cache_dir(env: ENV, home: Dir.home, host_os: RbConfig::CONFIG["host_os"])
56
77
  override = present_value(env["WGPU_CACHE_DIR"])
57
78
  return File.join(File.expand_path(override), VERSION) if override
@@ -62,24 +83,66 @@ module WGPU
62
83
  File.join(default_cache_base(env:, home:, host_os:), "wgpu-ruby", VERSION)
63
84
  end
64
85
 
86
+ # Returns the legacy versioned cache directory.
87
+ #
88
+ # @param home [String] user home directory
89
+ # @return [String] absolute legacy cache directory
65
90
  def legacy_cache_dir(home: Dir.home)
66
91
  File.join(File.expand_path(home), ".cache", "wgpu-ruby", VERSION)
67
92
  end
68
93
 
94
+ # Returns cache directories in lookup order.
95
+ #
96
+ # @param env [Hash] environment used for cache overrides
97
+ # @param home [String] user home directory
98
+ # @param host_os [String] host operating-system identifier
99
+ # @return [Array<String>] unique cache directories
69
100
  def cache_directories(env: ENV, home: Dir.home, host_os: RbConfig::CONFIG["host_os"])
70
101
  [primary_cache_dir(env:, home:, host_os:), legacy_cache_dir(home:)].uniq
71
102
  end
72
103
 
104
+ # Returns candidate cached library paths for a platform.
105
+ #
106
+ # @param platform [String] Ruby platform identifier
107
+ # @param env [Hash] environment used for cache overrides
108
+ # @param home [String] user home directory
109
+ # @param host_os [String] host operating-system identifier
110
+ # @return [Array<String>] candidate shared-library paths
111
+ # @raise [LoadError] if the platform is unsupported
73
112
  def library_paths(platform: RUBY_PLATFORM, env: ENV, home: Dir.home,
74
113
  host_os: RbConfig::CONFIG["host_os"])
75
114
  library = artifact_for(platform)[:library]
76
115
  cache_directories(env:, home:, host_os:).map { |directory| File.join(directory, "lib", library) }
77
116
  end
78
117
 
118
+ # Builds the download URL for an artifact entry.
119
+ #
120
+ # @param artifact [Hash] entry from {ARTIFACTS}
121
+ # @return [String] release archive URL
79
122
  def release_url(artifact)
80
123
  "#{RELEASE_BASE_URL}/#{artifact.fetch(:archive)}"
81
124
  end
82
125
 
126
+ # Decodes the packed native version into a release tag.
127
+ #
128
+ # @param encoded_version [Integer] four-byte packed version
129
+ # @return [String] version in +vMAJOR.MINOR.PATCH.BUILD+ form
130
+ def version_string(encoded_version)
131
+ components = [24, 16, 8, 0].map { |shift| (encoded_version >> shift) & 0xFF }
132
+ "v#{components.join(".")}"
133
+ end
134
+
135
+ # Reports whether a native capability is implemented by the pinned release.
136
+ # @param name [Symbol] capability name
137
+ # @return [Boolean]
138
+ def capability_implemented?(name)
139
+ !UNIMPLEMENTED_CAPABILITIES.fetch(name, []).include?(VERSION)
140
+ end
141
+
142
+ # Builds an actionable unsupported-platform message.
143
+ #
144
+ # @param platform [String] Ruby platform identifier
145
+ # @return [String] diagnostic message
83
146
  def unsupported_platform_message(platform)
84
147
  supported = ARTIFACTS.map { |artifact| artifact[:pattern].inspect }.join(", ")
85
148
  <<~MESSAGE.chomp
@@ -5,6 +5,12 @@ module WGPU
5
5
  module EnumHelper
6
6
  module_function
7
7
 
8
+ # Converts a symbolic enum member to its integer value.
9
+ # @param enum [FFI::Enum, Hash] enum mapping
10
+ # @param value [Symbol, Integer] member or native value
11
+ # @param name [String] name used in validation errors
12
+ # @return [Integer]
13
+ # @raise [ArgumentError] if the value is invalid
8
14
  def coerce(enum, value, name: "enum")
9
15
  return value if value.is_a?(Integer)
10
16
  raise ArgumentError, "#{name} must be a Symbol or Integer, got #{value.class}" unless value.is_a?(Symbol)
@@ -16,6 +22,13 @@ module WGPU
16
22
  raise ArgumentError, unknown_value_message(name, value, mapping.keys)
17
23
  end
18
24
 
25
+ # Converts symbolic flag names to their combined integer bitset.
26
+ #
27
+ # @param enum [FFI::Enum, Hash] flag mapping
28
+ # @param value [Symbol, Integer, Array<Symbol>] flags to convert
29
+ # @param name [String] name used in validation errors
30
+ # @return [Integer] combined bitset
31
+ # @raise [ArgumentError] if a flag is unknown or has an invalid type
19
32
  def coerce_flags(enum, value, name: "flags")
20
33
  return value if value.is_a?(Integer)
21
34
 
@@ -27,6 +40,10 @@ module WGPU
27
40
  values.reduce(0) { |flags, item| flags | coerce(enum, item, name:) }
28
41
  end
29
42
 
43
+ # Expands a bitset into its independent symbolic flags.
44
+ # @param enum [FFI::Enum, Hash] flag mapping
45
+ # @param value [Integer] combined bitset
46
+ # @return [Array<Symbol>]
30
47
  def decompose_flags(enum, value)
31
48
  raise ArgumentError, "flag value must be an Integer" unless value.is_a?(Integer)
32
49
 
@@ -61,6 +61,14 @@ module WGPU
61
61
  :unknown, 0x00000004
62
62
  )
63
63
 
64
+ CreatePipelineAsyncStatus = enum(
65
+ :success, 0x00000001,
66
+ :instance_dropped, 0x00000002,
67
+ :validation_error, 0x00000003,
68
+ :internal_error, 0x00000004,
69
+ :unknown, 0x00000005
70
+ )
71
+
64
72
  DeviceLostReason = enum(
65
73
  :unknown, 0x00000001,
66
74
  :destroyed, 0x00000002,