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.
- 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 +49 -20
- data/lib/wgpu/core/async_waiter.rb +92 -0
- data/lib/wgpu/core/canvas_context.rb +0 -2
- data/lib/wgpu/core/device.rb +160 -52
- data/lib/wgpu/core/instance.rb +9 -5
- data/lib/wgpu/core/queue.rb +81 -57
- 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 +9 -6
- data/lib/wgpu/native/capabilities.rb +31 -0
- data/lib/wgpu/native/distribution.rb +113 -0
- data/lib/wgpu/native/enum_helper.rb +63 -0
- data/lib/wgpu/native/enums.rb +82 -13
- data/lib/wgpu/native/functions.rb +17 -8
- data/lib/wgpu/native/installer.rb +192 -0
- data/lib/wgpu/native/loader.rb +40 -21
- data/lib/wgpu/native/structs.rb +19 -5
- 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 +51 -12
- data/lib/wgpu/resources/buffer.rb +167 -84
- 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 +33 -0
- data/sig/wgpu.rbs +381 -0
- 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
|
data/lib/wgpu/logging.rb
ADDED
|
@@ -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 :
|
|
15
|
-
[:
|
|
14
|
+
callback :uncaptured_error_callback,
|
|
15
|
+
[:pointer, ErrorType, StringView.by_value, :pointer, :pointer], :void
|
|
16
16
|
|
|
17
17
|
callback :device_lost_callback,
|
|
18
|
-
[:pointer,
|
|
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
|