wgpu 1.0.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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -0
  3. data/README.md +26 -3
  4. data/docs/README.md +22 -0
  5. data/docs/api_coverage.md +132 -0
  6. data/docs/async.md +31 -0
  7. data/docs/bind_groups.md +25 -0
  8. data/docs/buffer_data.md +37 -0
  9. data/docs/command_encoding.md +24 -0
  10. data/docs/errors.md +40 -0
  11. data/docs/getting_started_compute.md +61 -0
  12. data/docs/getting_started_rendering.md +62 -0
  13. data/docs/installation.md +94 -0
  14. data/docs/pipeline_descriptors.md +38 -0
  15. data/docs/releasing.md +22 -0
  16. data/docs/resource_lifetime.md +94 -0
  17. data/docs/shaders.md +32 -0
  18. data/docs/texture_readback.md +19 -0
  19. data/docs/troubleshooting.md +48 -0
  20. data/docs/upgrading_wgpu_native.md +26 -0
  21. data/ext/wgpu/extconf.rb +10 -142
  22. data/lib/wgpu/commands/command_buffer.rb +11 -0
  23. data/lib/wgpu/commands/command_encoder.rb +65 -8
  24. data/lib/wgpu/commands/compute_pass.rb +10 -0
  25. data/lib/wgpu/commands/render_bundle_encoder.rb +10 -3
  26. data/lib/wgpu/commands/render_pass.rb +34 -8
  27. data/lib/wgpu/core/adapter.rb +49 -20
  28. data/lib/wgpu/core/async_waiter.rb +92 -0
  29. data/lib/wgpu/core/canvas_context.rb +0 -2
  30. data/lib/wgpu/core/device.rb +160 -52
  31. data/lib/wgpu/core/instance.rb +9 -5
  32. data/lib/wgpu/core/queue.rb +81 -57
  33. data/lib/wgpu/core/surface.rb +16 -17
  34. data/lib/wgpu/data_types.rb +67 -0
  35. data/lib/wgpu/descriptor_helpers.rb +39 -0
  36. data/lib/wgpu/error.rb +55 -0
  37. data/lib/wgpu/logging.rb +63 -0
  38. data/lib/wgpu/native/abi_verifier.rb +109 -0
  39. data/lib/wgpu/native/callbacks.rb +9 -6
  40. data/lib/wgpu/native/capabilities.rb +31 -0
  41. data/lib/wgpu/native/distribution.rb +113 -0
  42. data/lib/wgpu/native/enum_helper.rb +63 -0
  43. data/lib/wgpu/native/enums.rb +82 -13
  44. data/lib/wgpu/native/functions.rb +17 -8
  45. data/lib/wgpu/native/installer.rb +192 -0
  46. data/lib/wgpu/native/loader.rb +40 -21
  47. data/lib/wgpu/native/structs.rb +19 -5
  48. data/lib/wgpu/native_resource.rb +171 -0
  49. data/lib/wgpu/pipeline/bind_group.rb +12 -0
  50. data/lib/wgpu/pipeline/bind_group_layout.rb +91 -37
  51. data/lib/wgpu/pipeline/compute_pipeline.rb +28 -26
  52. data/lib/wgpu/pipeline/render_pipeline.rb +180 -68
  53. data/lib/wgpu/pipeline/shader_module.rb +51 -12
  54. data/lib/wgpu/resources/buffer.rb +167 -84
  55. data/lib/wgpu/resources/query_set.rb +14 -12
  56. data/lib/wgpu/resources/sampler.rb +36 -20
  57. data/lib/wgpu/resources/texture.rb +44 -42
  58. data/lib/wgpu/resources/texture_view.rb +11 -3
  59. data/lib/wgpu/texture_format.rb +82 -0
  60. data/lib/wgpu/version.rb +1 -1
  61. data/lib/wgpu/window.rb +8 -1
  62. data/lib/wgpu.rb +33 -0
  63. data/sig/wgpu.rbs +381 -0
  64. metadata +33 -16
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WGPU
4
+ module DataTypes
5
+ FORMATS = {
6
+ f32: ["e*", 4],
7
+ f64: ["E*", 8],
8
+ u32: ["L<*", 4],
9
+ i32: ["l<*", 4],
10
+ u16: ["S<*", 2],
11
+ u8: ["C*", 1]
12
+ }.freeze
13
+
14
+ module_function
15
+
16
+ def pack(values, type: :f32)
17
+ format, = format_for(type)
18
+ Array(values).pack(format)
19
+ rescue RangeError => e
20
+ raise ArgumentError, "value is out of range for #{type}: #{e.message}"
21
+ end
22
+
23
+ def unpack(bytes, type: :f32)
24
+ format, byte_size = format_for(type)
25
+ unless (bytes.bytesize % byte_size).zero?
26
+ raise ArgumentError, "#{type} data size must be a multiple of #{byte_size} bytes"
27
+ end
28
+
29
+ bytes.unpack(format)
30
+ end
31
+
32
+ def byte_size(type)
33
+ format_for(type).last
34
+ end
35
+
36
+ def to_pointer(data, type: :f32)
37
+ return [data, pointer_size(data)] if data.is_a?(FFI::Pointer)
38
+
39
+ bytes = data.is_a?(String) ? data : pack(data, type:)
40
+ pointer = FFI::MemoryPointer.new(:char, bytes.bytesize)
41
+ pointer.put_bytes(0, bytes)
42
+ [pointer, bytes.bytesize]
43
+ end
44
+
45
+ def validate_alignment!(value, alignment, name:)
46
+ integer = Integer(value)
47
+ raise ArgumentError, "#{name} must be non-negative" if integer.negative?
48
+ return integer if (integer % alignment).zero?
49
+
50
+ raise ArgumentError, "#{name} must be aligned to #{alignment} bytes (got #{integer})"
51
+ end
52
+
53
+ def format_for(type)
54
+ FORMATS.fetch(type.to_sym)
55
+ rescue NoMethodError, KeyError
56
+ raise ArgumentError, "Unknown data type #{type.inspect}. Valid values: #{FORMATS.keys.map(&:inspect).join(", ")}"
57
+ end
58
+ private_class_method :format_for
59
+
60
+ def pointer_size(pointer)
61
+ pointer.size
62
+ rescue NoMethodError
63
+ raise ArgumentError, "FFI pointer must have a known size; pass size explicitly"
64
+ end
65
+ private_class_method :pointer_size
66
+ end
67
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WGPU
4
+ module DescriptorHelpers
5
+ module_function
6
+
7
+ def set_label(descriptor, label, keepalive:)
8
+ if label
9
+ pointer = FFI::MemoryPointer.from_string(label)
10
+ keepalive << pointer
11
+ descriptor[:label][:data] = pointer
12
+ descriptor[:label][:length] = label.bytesize
13
+ else
14
+ descriptor[:label][:data] = nil
15
+ descriptor[:label][:length] = 0
16
+ end
17
+ end
18
+
19
+ def uint32_array(values, keepalive:)
20
+ return nil if values.empty?
21
+
22
+ pointer = FFI::MemoryPointer.new(:uint32, values.length)
23
+ pointer.write_array_of_uint32(values)
24
+ keepalive << pointer
25
+ pointer
26
+ end
27
+
28
+ def validate_keys!(options, allowed:, required: [], context: "descriptor")
29
+ return options unless options.is_a?(Hash)
30
+
31
+ missing = required - options.keys
32
+ raise ArgumentError, "#{context} is missing required keys: #{missing.map(&:inspect).join(", ")}" unless missing.empty?
33
+
34
+ unknown = options.keys - allowed
35
+ warn "Unknown #{context} keys: #{unknown.map(&:inspect).join(", ")}" unless unknown.empty?
36
+ options
37
+ end
38
+ end
39
+ end
data/lib/wgpu/error.rb CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  module WGPU
4
4
  class Error < StandardError; end
5
+ class ValidationError < Error; end
6
+ class OutOfMemoryError < Error; end
7
+ class InternalError < Error; end
8
+ class DeviceLostError < Error; end
9
+ class TimeoutError < Error; end
5
10
  class InitializationError < Error; end
6
11
  class AdapterError < Error; end
7
12
  class DeviceError < Error; end
@@ -12,5 +17,55 @@ module WGPU
12
17
  class PipelineError < Error; end
13
18
  class CommandError < Error; end
14
19
  class SurfaceError < Error; end
20
+ class SurfaceAcquisitionError < SurfaceError
21
+ attr_reader :status
22
+
23
+ def initialize(status, message = nil)
24
+ @status = status
25
+ super(message || "Failed to get current surface texture: #{status}")
26
+ end
27
+ end
15
28
  class RenderBundleError < Error; end
29
+
30
+ GPUError = Data.define(:type, :message) do
31
+ def self.from_hash(error)
32
+ return if error.nil? || error[:type].nil? || error[:type] == :no_error
33
+
34
+ new(type: error[:type], message: error[:message].to_s)
35
+ end
36
+
37
+ def exception_class
38
+ {
39
+ validation: ValidationError,
40
+ out_of_memory: OutOfMemoryError,
41
+ internal: InternalError,
42
+ device_lost: DeviceLostError
43
+ }.fetch(type, Error)
44
+ end
45
+
46
+ def raise!
47
+ raise exception_class, "GPU error (#{type}): #{message}"
48
+ end
49
+
50
+ def to_h
51
+ { type:, message: }
52
+ end
53
+ end
54
+
55
+ CompilationMessage = Data.define(
56
+ :type,
57
+ :message,
58
+ :line_num,
59
+ :line_pos,
60
+ :offset,
61
+ :length
62
+ ) do
63
+ alias line line_num
64
+ alias column line_pos
65
+
66
+ def to_s
67
+ location = line_num&.positive? ? "#{line_num}:#{line_pos}" : "unknown location"
68
+ "#{location}: #{type}: #{message}"
69
+ end
70
+ end
16
71
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WGPU
4
+ class << self
5
+ # Returns the configured wgpu-native log level.
6
+ attr_reader :log_level
7
+
8
+ # Sets the wgpu-native log level.
9
+ #
10
+ # @param level [Symbol, Integer] one of `:off`, `:error`, `:warn`, `:info`,
11
+ # `:debug`, or `:trace`
12
+ # @return [Symbol, Integer] the supplied level
13
+ def log_level=(level)
14
+ ensure_native_logging!
15
+ value = Native::EnumHelper.coerce(Native::LogLevel, level, name: "log level")
16
+ Native.wgpuSetLogLevel(value)
17
+ @log_level = Native::LogLevel[value]
18
+ end
19
+
20
+ # Registers a process-wide wgpu-native log handler.
21
+ #
22
+ # The native callback is retained by the WGPU module for the rest of the
23
+ # process, or until this method replaces it.
24
+ #
25
+ # @yieldparam level [Symbol] native log severity
26
+ # @yieldparam message [String] UTF-8 log message
27
+ # @return [WGPU]
28
+ def on_log(&handler)
29
+ raise ArgumentError, "block is required" unless handler
30
+
31
+ ensure_native_logging!
32
+ @log_handler = handler
33
+ @native_log_callback = FFI::Function.new(
34
+ :void,
35
+ [:uint32, Native::StringView.by_value, :pointer]
36
+ ) do |level, message, _userdata|
37
+ text =
38
+ if message[:data] && !message[:data].null? && message[:length].positive?
39
+ message[:data].read_string(message[:length])
40
+ else
41
+ ""
42
+ end
43
+ @log_handler.call(Native::LogLevel[level] || level, text)
44
+ rescue StandardError => e
45
+ warn "WGPU log handler failed: #{e.class}: #{e.message}"
46
+ end
47
+ Native.wgpuSetLogCallback(@native_log_callback, nil)
48
+ self
49
+ end
50
+
51
+ private
52
+
53
+ def ensure_native_logging!
54
+ return if Native.logging_available?
55
+
56
+ raise Error,
57
+ "wgpu-native logging is unavailable in the loaded library " \
58
+ "(expected #{Native::Distribution::VERSION})"
59
+ end
60
+ end
61
+
62
+ @log_level = :warn
63
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WGPU
4
+ module Native
5
+ class AbiVerifier
6
+ FORCE32_KEY = "force32"
7
+ EXTENSION_ENTRIES = {
8
+ "SType" => %w[invalid shadersourceglsl],
9
+ "FeatureName" => %w[
10
+ pushconstants textureadapterspecificformatfeatures multidrawindirectcount
11
+ vertexwritablestorage texturebindingarray
12
+ sampledtextureandstoragebufferarraynonuniformindexing
13
+ pipelinestatisticsquery storageresourcebindingarray
14
+ partiallyboundbindingarray textureformat16bitnorm
15
+ texturecompressionastchdr mappableprimarybuffers bufferbindingarray
16
+ uniformbufferandstoragetexturearraynonuniformindexing polygonmodeline
17
+ polygonmodepoint conservativerasterization spirvshaderpassthrough
18
+ vertexattribute64bit textureformatnv12 rayquery shaderf64 shaderi16
19
+ shaderprimitiveindex shaderearlydepthtest subgroup subgroupvertex
20
+ subgroupbarrier timestampqueryinsideencoders timestampqueryinsidepasses
21
+ shaderint64
22
+ ]
23
+ }.freeze
24
+
25
+ def initialize(header_path: nil, native: Native)
26
+ @header_path = header_path || self.class.default_header_path
27
+ @native = native
28
+ end
29
+
30
+ def verify!
31
+ differences = enum_differences
32
+ return true if differences.empty?
33
+
34
+ raise WGPU::Error, "wgpu-native ABI enum differences:\n#{differences.join("\n")}"
35
+ end
36
+
37
+ def enum_differences
38
+ header_enums = parse_header
39
+ ruby_enums.filter_map do |name, mapping|
40
+ header_mapping = header_enums[name]
41
+ next unless header_mapping
42
+
43
+ compare_enum(name, ruby_mapping(mapping), header_mapping)
44
+ end
45
+ end
46
+
47
+ def self.default_header_path
48
+ override = ENV["WGPU_HEADER_PATH"]
49
+ return File.expand_path(override) if override && !override.empty?
50
+
51
+ candidates = Distribution.cache_directories.map do |cache_dir|
52
+ File.join(cache_dir, "include", "webgpu", "webgpu.h")
53
+ end
54
+ path = candidates.find { |candidate| File.file?(candidate) }
55
+ return path if path
56
+
57
+ raise WGPU::Error, <<~MSG.chomp
58
+ Pinned #{Distribution::VERSION} webgpu.h was not found.
59
+ Run `bundle exec rake wgpu:install`, or set WGPU_HEADER_PATH.
60
+ Searched: #{candidates.join(", ")}
61
+ MSG
62
+ end
63
+
64
+ private
65
+
66
+ def parse_header
67
+ source = File.read(@header_path)
68
+ source.scan(
69
+ /typedef enum WGPU(\w+)\s*\{(.*?)\}\s*WGPU\1(?:\s+WGPU_ENUM_ATTRIBUTE)?;/m
70
+ ).to_h do |name, body|
71
+ entries = body.scan(
72
+ /WGPU#{Regexp.escape(name)}_([A-Za-z0-9_]+)\s*=\s*(0x[0-9A-Fa-f]+|\d+)/
73
+ ).to_h { |entry, value| [normalize(entry), Integer(value)] }
74
+ entries.delete(FORCE32_KEY)
75
+ [name, entries]
76
+ end
77
+ end
78
+
79
+ def ruby_enums
80
+ @native.constants(false).sort.filter_map do |constant_name|
81
+ value = @native.const_get(constant_name)
82
+ [constant_name.to_s, value.to_h] if value.is_a?(FFI::Enum)
83
+ end
84
+ end
85
+
86
+ def ruby_mapping(mapping)
87
+ mapping.to_h { |key, value| [normalize(key), value] }
88
+ end
89
+
90
+ def normalize(name)
91
+ normalized = name.to_s.delete("_").downcase
92
+ normalized.sub(/\A([123])d/, 'd\1')
93
+ end
94
+
95
+ def compare_enum(name, ruby_mapping, header_mapping)
96
+ missing = header_mapping.keys - ruby_mapping.keys
97
+ extra = ruby_mapping.keys - header_mapping.keys - EXTENSION_ENTRIES.fetch(name, [])
98
+ wrong = (header_mapping.keys & ruby_mapping.keys).filter_map do |key|
99
+ next if header_mapping[key] == ruby_mapping[key]
100
+
101
+ "#{key}=#{ruby_mapping[key]} (header #{header_mapping[key]})"
102
+ end
103
+ return if missing.empty? && extra.empty? && wrong.empty?
104
+
105
+ "#{name}: missing=#{missing.inspect}, extra=#{extra.inspect}, mismatched=#{wrong.inspect}"
106
+ end
107
+ end
108
+ end
109
+ end
@@ -3,24 +3,27 @@
3
3
  module WGPU
4
4
  module Native
5
5
  callback :request_adapter_callback,
6
- [RequestAdapterStatus, :pointer, StringView.by_value, :pointer], :void
6
+ [RequestAdapterStatus, :pointer, StringView.by_value, :pointer, :pointer], :void
7
7
 
8
8
  callback :request_device_callback,
9
- [RequestDeviceStatus, :pointer, StringView.by_value, :pointer], :void
9
+ [RequestDeviceStatus, :pointer, StringView.by_value, :pointer, :pointer], :void
10
10
 
11
11
  callback :buffer_map_callback,
12
- [MapAsyncStatus, :pointer], :void
12
+ [MapAsyncStatus, StringView.by_value, :pointer, :pointer], :void
13
13
 
14
- callback :error_callback,
15
- [:uint32, StringView.by_value, :pointer], :void
14
+ callback :uncaptured_error_callback,
15
+ [:pointer, ErrorType, StringView.by_value, :pointer, :pointer], :void
16
16
 
17
17
  callback :device_lost_callback,
18
- [:pointer, :uint32, StringView.by_value, :pointer], :void
18
+ [:pointer, DeviceLostReason, StringView.by_value, :pointer, :pointer], :void
19
19
 
20
20
  callback :pop_error_scope_callback,
21
21
  [PopErrorScopeStatus, ErrorType, StringView.by_value, :pointer, :pointer], :void
22
22
 
23
23
  callback :queue_work_done_callback,
24
24
  [QueueWorkDoneStatus, :pointer, :pointer], :void
25
+
26
+ callback :log_callback,
27
+ [LogLevel, StringView.by_value, :pointer], :void
25
28
  end
26
29
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WGPU
4
+ module Native
5
+ class << self
6
+ def future_api?
7
+ optional_function_available?(:wgpuInstanceWaitAny)
8
+ end
9
+
10
+ def device_poll_available?
11
+ optional_function_available?(:wgpuDevicePoll)
12
+ end
13
+
14
+ # wgpu-native v27 exports wgpuShaderModuleGetCompilationInfo, but the
15
+ # implementation is a Rust panic stub. Calling it aborts the process, so
16
+ # symbol presence alone cannot be used as a capability check.
17
+ def compilation_info_available?
18
+ Distribution::VERSION != "v27.0.4.0"
19
+ end
20
+
21
+ def buffer_map_state_available?
22
+ Distribution::VERSION != "v27.0.4.0"
23
+ end
24
+
25
+ def logging_available?
26
+ optional_function_available?(:wgpuSetLogCallback) &&
27
+ optional_function_available?(:wgpuSetLogLevel)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+
5
+ module WGPU
6
+ module Native
7
+ module Distribution
8
+ VERSION = "v27.0.4.0"
9
+ RELEASE_BASE_URL = "https://github.com/gfx-rs/wgpu-native/releases/download/#{VERSION}"
10
+
11
+ ARTIFACTS = [
12
+ {
13
+ pattern: /x86_64-linux/,
14
+ archive: "wgpu-linux-x86_64-release.zip",
15
+ library: "libwgpu_native.so",
16
+ sha256: "271481ef76fbf3ea09631a6079e9493636ecf813cd9c92306c44a1a452991ba1"
17
+ },
18
+ {
19
+ pattern: /aarch64-linux/,
20
+ archive: "wgpu-linux-aarch64-release.zip",
21
+ library: "libwgpu_native.so",
22
+ sha256: "a2f22248200997b69373273b10d50a58164f6ed840877289f3e46bff317b134e"
23
+ },
24
+ {
25
+ pattern: /x86_64-darwin/,
26
+ archive: "wgpu-macos-x86_64-release.zip",
27
+ library: "libwgpu_native.dylib",
28
+ sha256: "660fe9be59b555ec1d7c839e5cf8b6c71762938af61ab444a7a58dd87970dba2"
29
+ },
30
+ {
31
+ pattern: /arm64-darwin/,
32
+ archive: "wgpu-macos-aarch64-release.zip",
33
+ library: "libwgpu_native.dylib",
34
+ sha256: "15367c26fdbe6892db35007d39f3883593384e777360b70e6bd704cb5dedde53"
35
+ },
36
+ {
37
+ pattern: /(?:x64|x86_64)-(?:mingw|mswin)/,
38
+ archive: "wgpu-windows-x86_64-msvc-release.zip",
39
+ library: "wgpu_native.dll",
40
+ sha256: "f14ca334b4d253881bde2605bd147f332178d705f56fbd74f81458797c77fce1"
41
+ }
42
+ ].map(&:freeze).freeze
43
+
44
+ module_function
45
+
46
+ def artifact_for(platform = RUBY_PLATFORM)
47
+ raise LoadError, unsupported_platform_message(platform) if platform.include?("musl")
48
+
49
+ artifact = ARTIFACTS.find { |candidate| candidate[:pattern].match?(platform) }
50
+ return artifact if artifact
51
+
52
+ raise LoadError, unsupported_platform_message(platform)
53
+ end
54
+
55
+ def primary_cache_dir(env: ENV, home: Dir.home, host_os: RbConfig::CONFIG["host_os"])
56
+ override = present_value(env["WGPU_CACHE_DIR"])
57
+ return File.join(File.expand_path(override), VERSION) if override
58
+
59
+ xdg_cache = present_value(env["XDG_CACHE_HOME"])
60
+ return File.join(File.expand_path(xdg_cache), "wgpu-ruby", VERSION) if xdg_cache
61
+
62
+ File.join(default_cache_base(env:, home:, host_os:), "wgpu-ruby", VERSION)
63
+ end
64
+
65
+ def legacy_cache_dir(home: Dir.home)
66
+ File.join(File.expand_path(home), ".cache", "wgpu-ruby", VERSION)
67
+ end
68
+
69
+ def cache_directories(env: ENV, home: Dir.home, host_os: RbConfig::CONFIG["host_os"])
70
+ [primary_cache_dir(env:, home:, host_os:), legacy_cache_dir(home:)].uniq
71
+ end
72
+
73
+ def library_paths(platform: RUBY_PLATFORM, env: ENV, home: Dir.home,
74
+ host_os: RbConfig::CONFIG["host_os"])
75
+ library = artifact_for(platform)[:library]
76
+ cache_directories(env:, home:, host_os:).map { |directory| File.join(directory, "lib", library) }
77
+ end
78
+
79
+ def release_url(artifact)
80
+ "#{RELEASE_BASE_URL}/#{artifact.fetch(:archive)}"
81
+ end
82
+
83
+ def unsupported_platform_message(platform)
84
+ supported = ARTIFACTS.map { |artifact| artifact[:pattern].inspect }.join(", ")
85
+ <<~MESSAGE.chomp
86
+ Unsupported platform: #{platform}
87
+ Supported 64-bit platforms: #{supported}
88
+ Build wgpu-native for this host and set WGPU_LIB_PATH to the shared library.
89
+ MESSAGE
90
+ end
91
+
92
+ def default_cache_base(env:, home:, host_os:)
93
+ case host_os
94
+ when /darwin/
95
+ File.join(File.expand_path(home), "Library", "Caches")
96
+ when /mingw|mswin/
97
+ local_app_data = present_value(env["LOCALAPPDATA"])
98
+ local_app_data || File.join(File.expand_path(home), "AppData", "Local")
99
+ else
100
+ File.join(File.expand_path(home), ".cache")
101
+ end
102
+ end
103
+ private_class_method :default_cache_base
104
+
105
+ def present_value(value)
106
+ value unless value.nil? || value.empty?
107
+ end
108
+ private_class_method :present_value
109
+ end
110
+
111
+ WGPU_VERSION = Distribution::VERSION
112
+ end
113
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WGPU
4
+ module Native
5
+ module EnumHelper
6
+ module_function
7
+
8
+ def coerce(enum, value, name: "enum")
9
+ return value if value.is_a?(Integer)
10
+ raise ArgumentError, "#{name} must be a Symbol or Integer, got #{value.class}" unless value.is_a?(Symbol)
11
+
12
+ mapping = mapping_for(enum)
13
+ result = mapping[value]
14
+ return result unless result.nil?
15
+
16
+ raise ArgumentError, unknown_value_message(name, value, mapping.keys)
17
+ end
18
+
19
+ def coerce_flags(enum, value, name: "flags")
20
+ return value if value.is_a?(Integer)
21
+
22
+ values = value.is_a?(Array) ? value : [value]
23
+ unless values.all? { |item| item.is_a?(Symbol) }
24
+ raise ArgumentError, "#{name} must be a Symbol, Integer, or Array of Symbols"
25
+ end
26
+
27
+ values.reduce(0) { |flags, item| flags | coerce(enum, item, name:) }
28
+ end
29
+
30
+ def decompose_flags(enum, value)
31
+ raise ArgumentError, "flag value must be an Integer" unless value.is_a?(Integer)
32
+
33
+ mapping = mapping_for(enum)
34
+ return [:none] if value.zero? && mapping.key?(:none)
35
+
36
+ mapping.filter_map do |symbol, bit|
37
+ symbol if bit.positive? && power_of_two?(bit) && (value & bit) == bit
38
+ end
39
+ end
40
+
41
+ def mapping_for(enum)
42
+ mapping = enum.respond_to?(:to_h) ? enum.to_h : enum
43
+ unless mapping.respond_to?(:key?) && mapping.respond_to?(:keys)
44
+ raise ArgumentError, "enum must be an FFI::Enum or Hash"
45
+ end
46
+
47
+ mapping
48
+ end
49
+ private_class_method :mapping_for
50
+
51
+ def unknown_value_message(name, value, candidates)
52
+ formatted = candidates.grep(Symbol).sort.map(&:inspect).join(", ")
53
+ "Unknown #{name} #{value.inspect}. Valid values: #{formatted}"
54
+ end
55
+ private_class_method :unknown_value_message
56
+
57
+ def power_of_two?(value)
58
+ (value & (value - 1)).zero?
59
+ end
60
+ private_class_method :power_of_two?
61
+ end
62
+ end
63
+ end