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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +88 -0
- data/README.md +44 -5
- data/docs/README.md +22 -0
- data/docs/api_coverage.md +140 -0
- data/docs/async.md +41 -0
- data/docs/bind_groups.md +25 -0
- data/docs/buffer_data.md +37 -0
- data/docs/command_encoding.md +49 -0
- data/docs/errors.md +40 -0
- data/docs/getting_started_compute.md +62 -0
- data/docs/getting_started_rendering.md +62 -0
- data/docs/installation.md +94 -0
- data/docs/pipeline_descriptors.md +43 -0
- data/docs/releasing.md +32 -0
- data/docs/resource_lifetime.md +108 -0
- data/docs/shaders.md +32 -0
- data/docs/texture_readback.md +46 -0
- data/docs/troubleshooting.md +53 -0
- data/docs/upgrading_wgpu_native.md +37 -0
- data/ext/wgpu/extconf.rb +10 -142
- data/lib/wgpu/async_task.rb +19 -0
- data/lib/wgpu/commands/command_buffer.rb +25 -1
- data/lib/wgpu/commands/command_encoder.rb +123 -9
- data/lib/wgpu/commands/compute_pass.rb +53 -0
- data/lib/wgpu/commands/render_bundle.rb +9 -1
- data/lib/wgpu/commands/render_bundle_encoder.rb +65 -4
- data/lib/wgpu/commands/render_pass.rb +136 -8
- data/lib/wgpu/core/adapter.rb +123 -19
- data/lib/wgpu/core/async_waiter.rb +47 -4
- data/lib/wgpu/core/canvas_context.rb +32 -2
- data/lib/wgpu/core/device.rb +399 -53
- data/lib/wgpu/core/instance.rb +28 -4
- data/lib/wgpu/core/queue.rb +197 -51
- data/lib/wgpu/core/surface.rb +64 -17
- data/lib/wgpu/data_types.rb +83 -0
- data/lib/wgpu/descriptor_helpers.rb +104 -0
- data/lib/wgpu/error.rb +70 -0
- data/lib/wgpu/logging.rb +63 -0
- data/lib/wgpu/native/abi_verifier.rb +143 -0
- data/lib/wgpu/native/callbacks.rb +10 -1
- data/lib/wgpu/native/capabilities.rb +32 -2
- data/lib/wgpu/native/distribution.rb +176 -0
- data/lib/wgpu/native/enum_helper.rb +80 -0
- data/lib/wgpu/native/enums.rb +68 -8
- data/lib/wgpu/native/fixtures/webgpu-v27.0.4.0-enums.h +848 -0
- data/lib/wgpu/native/functions.rb +21 -10
- data/lib/wgpu/native/installer.rb +223 -0
- data/lib/wgpu/native/loader.rb +61 -21
- data/lib/wgpu/native/structs.rb +18 -1
- data/lib/wgpu/native_resource.rb +342 -0
- data/lib/wgpu/pipeline/bind_group.rb +21 -0
- data/lib/wgpu/pipeline/bind_group_layout.rb +108 -41
- data/lib/wgpu/pipeline/compute_pipeline.rb +40 -50
- data/lib/wgpu/pipeline/pipeline_layout.rb +8 -0
- data/lib/wgpu/pipeline/render_pipeline.rb +207 -110
- data/lib/wgpu/pipeline/shader_module.rb +95 -28
- data/lib/wgpu/resources/buffer.rb +387 -85
- data/lib/wgpu/resources/query_set.rb +26 -12
- data/lib/wgpu/resources/sampler.rb +43 -20
- data/lib/wgpu/resources/texture.rb +89 -48
- data/lib/wgpu/resources/texture_view.rb +35 -7
- data/lib/wgpu/texture_format.rb +96 -0
- data/lib/wgpu/version.rb +1 -1
- data/lib/wgpu/window.rb +34 -1
- data/lib/wgpu.rb +32 -0
- data/sig/wgpu.rbs +460 -0
- metadata +33 -16
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
# Encodes Ruby values in little-endian native transfer format.
|
|
17
|
+
# @param values [Array] values to encode
|
|
18
|
+
# @param type [Symbol] element type
|
|
19
|
+
# @return [String] encoded bytes
|
|
20
|
+
def pack(values, type: :f32)
|
|
21
|
+
format, = format_for(type)
|
|
22
|
+
Array(values).pack(format)
|
|
23
|
+
rescue RangeError => e
|
|
24
|
+
raise ArgumentError, "value is out of range for #{type}: #{e.message}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Decodes little-endian transfer bytes into Ruby values.
|
|
28
|
+
# @param bytes [String] encoded bytes
|
|
29
|
+
# @param type [Symbol] element type
|
|
30
|
+
# @return [Array]
|
|
31
|
+
def unpack(bytes, type: :f32)
|
|
32
|
+
format, byte_size = format_for(type)
|
|
33
|
+
unless (bytes.bytesize % byte_size).zero?
|
|
34
|
+
raise ArgumentError, "#{type} data size must be a multiple of #{byte_size} bytes"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
bytes.unpack(format)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Returns the byte width of one typed value.
|
|
41
|
+
# @param type [Symbol] element type
|
|
42
|
+
# @return [Integer]
|
|
43
|
+
def byte_size(type)
|
|
44
|
+
format_for(type).last
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Converts data to an FFI pointer and reports its byte length.
|
|
48
|
+
# @return [Array(FFI::Pointer, Integer)] pointer and byte length
|
|
49
|
+
def to_pointer(data, type: :f32)
|
|
50
|
+
return [data, pointer_size(data)] if data.is_a?(FFI::Pointer)
|
|
51
|
+
|
|
52
|
+
bytes = data.is_a?(String) ? data : pack(data, type:)
|
|
53
|
+
pointer = FFI::MemoryPointer.new(:char, bytes.bytesize)
|
|
54
|
+
pointer.put_bytes(0, bytes)
|
|
55
|
+
[pointer, bytes.bytesize]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Validates a non-negative aligned byte value.
|
|
59
|
+
# @return [Integer] normalized value
|
|
60
|
+
# @raise [ArgumentError] if negative or misaligned
|
|
61
|
+
def validate_alignment!(value, alignment, name:)
|
|
62
|
+
integer = Integer(value)
|
|
63
|
+
raise ArgumentError, "#{name} must be non-negative" if integer.negative?
|
|
64
|
+
return integer if (integer % alignment).zero?
|
|
65
|
+
|
|
66
|
+
raise ArgumentError, "#{name} must be aligned to #{alignment} bytes (got #{integer})"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def format_for(type)
|
|
70
|
+
FORMATS.fetch(type.to_sym)
|
|
71
|
+
rescue NoMethodError, KeyError
|
|
72
|
+
raise ArgumentError, "Unknown data type #{type.inspect}. Valid values: #{FORMATS.keys.map(&:inspect).join(", ")}"
|
|
73
|
+
end
|
|
74
|
+
private_class_method :format_for
|
|
75
|
+
|
|
76
|
+
def pointer_size(pointer)
|
|
77
|
+
pointer.size
|
|
78
|
+
rescue NoMethodError
|
|
79
|
+
raise ArgumentError, "FFI pointer must have a known size; pass size explicitly"
|
|
80
|
+
end
|
|
81
|
+
private_class_method :pointer_size
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module WGPU
|
|
4
|
+
module DescriptorHelpers
|
|
5
|
+
SIZE_MAX = (1 << (FFI.type_size(:size_t) * 8)) - 1
|
|
6
|
+
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# Writes an optional Ruby label into a native descriptor.
|
|
10
|
+
#
|
|
11
|
+
# @param descriptor [FFI::Struct] descriptor with a +label+ member
|
|
12
|
+
# @param label [String, nil] label text
|
|
13
|
+
# @param keepalive [Array] receives allocated pointers that must remain alive
|
|
14
|
+
# @return [void]
|
|
15
|
+
def set_label(descriptor, label, keepalive:)
|
|
16
|
+
if label
|
|
17
|
+
pointer = FFI::MemoryPointer.from_string(label)
|
|
18
|
+
keepalive << pointer
|
|
19
|
+
descriptor[:label][:data] = pointer
|
|
20
|
+
descriptor[:label][:length] = label.bytesize
|
|
21
|
+
else
|
|
22
|
+
descriptor[:label][:data] = nil
|
|
23
|
+
descriptor[:label][:length] = 0
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Allocates and fills a native uint32 array.
|
|
28
|
+
#
|
|
29
|
+
# @param values [Array<Integer>] values to copy
|
|
30
|
+
# @param keepalive [Array] receives the allocated pointer
|
|
31
|
+
# @return [FFI::MemoryPointer, nil] pointer, or +nil+ for an empty array
|
|
32
|
+
def uint32_array(values, keepalive:)
|
|
33
|
+
return nil if values.empty?
|
|
34
|
+
|
|
35
|
+
pointer = FFI::MemoryPointer.new(:uint32, values.length)
|
|
36
|
+
pointer.write_array_of_uint32(values)
|
|
37
|
+
keepalive << pointer
|
|
38
|
+
pointer
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Writes a nullable string into a native string view.
|
|
42
|
+
#
|
|
43
|
+
# @param string_view [Native::StringView] destination view
|
|
44
|
+
# @param value [String, nil] string value; +nil+ uses the native null sentinel
|
|
45
|
+
# @param keepalive [Array] receives allocated pointers that must remain alive
|
|
46
|
+
# @return [void]
|
|
47
|
+
def set_nullable_string_view(string_view, value, keepalive:)
|
|
48
|
+
if value.nil?
|
|
49
|
+
string_view[:data] = nil
|
|
50
|
+
string_view[:length] = SIZE_MAX
|
|
51
|
+
return
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
string = value
|
|
55
|
+
pointer = FFI::MemoryPointer.from_string(string)
|
|
56
|
+
keepalive << pointer
|
|
57
|
+
string_view[:data] = pointer
|
|
58
|
+
string_view[:length] = string.bytesize
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Writes pipeline-overridable constants into a stage descriptor.
|
|
62
|
+
#
|
|
63
|
+
# @param stage_descriptor [FFI::Struct] programmable stage descriptor
|
|
64
|
+
# @param constants [Hash{#to_s => Numeric}, nil] constant names and values
|
|
65
|
+
# @param keepalive [Array] receives allocated pointers that must remain alive
|
|
66
|
+
# @return [void]
|
|
67
|
+
def set_constants(stage_descriptor, constants, keepalive:)
|
|
68
|
+
stage_descriptor[:constant_count] = 0
|
|
69
|
+
stage_descriptor[:constants] = nil
|
|
70
|
+
return if constants.nil? || constants.empty?
|
|
71
|
+
|
|
72
|
+
constants_pointer = FFI::MemoryPointer.new(Native::ConstantEntry, constants.size)
|
|
73
|
+
keepalive << constants_pointer
|
|
74
|
+
|
|
75
|
+
constants.each_with_index do |(key, value), index|
|
|
76
|
+
entry_pointer = constants_pointer + (index * Native::ConstantEntry.size)
|
|
77
|
+
entry = Native::ConstantEntry.new(entry_pointer)
|
|
78
|
+
entry[:next_in_chain] = nil
|
|
79
|
+
set_nullable_string_view(entry[:key], key.to_s, keepalive:)
|
|
80
|
+
entry[:value] = value.to_f
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
stage_descriptor[:constant_count] = constants.size
|
|
84
|
+
stage_descriptor[:constants] = constants_pointer
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Checks required keys and warns about unsupported descriptor keys.
|
|
88
|
+
# @param options [Hash] descriptor options
|
|
89
|
+
# @param allowed [Array<Symbol>] supported keys
|
|
90
|
+
# @param required [Array<Symbol>] mandatory keys
|
|
91
|
+
# @return [Hash] original options
|
|
92
|
+
# @raise [ArgumentError] when required keys are missing
|
|
93
|
+
def validate_keys!(options, allowed:, required: [], context: "descriptor")
|
|
94
|
+
return options unless options.is_a?(Hash)
|
|
95
|
+
|
|
96
|
+
missing = required - options.keys
|
|
97
|
+
raise ArgumentError, "#{context} is missing required keys: #{missing.map(&:inspect).join(", ")}" unless missing.empty?
|
|
98
|
+
|
|
99
|
+
unknown = options.keys - allowed
|
|
100
|
+
warn "Unknown #{context} keys: #{unknown.map(&:inspect).join(", ")}" unless unknown.empty?
|
|
101
|
+
options
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
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,70 @@ 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
|
+
# Creates an acquisition error for a native surface status.
|
|
24
|
+
# @param status [Symbol] native acquisition status
|
|
25
|
+
# @param message [String, nil] optional override message
|
|
26
|
+
def initialize(status, message = nil)
|
|
27
|
+
@status = status
|
|
28
|
+
super(message || "Failed to get current surface texture: #{status}")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
15
31
|
class RenderBundleError < Error; end
|
|
32
|
+
|
|
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]
|
|
37
|
+
def self.from_hash(error)
|
|
38
|
+
return if error.nil? || error[:type].nil? || error[:type] == :no_error
|
|
39
|
+
|
|
40
|
+
new(type: error[:type], message: error[:message].to_s)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Returns the Ruby exception class matching this GPU error type.
|
|
44
|
+
# @return [Class<Error>]
|
|
45
|
+
def exception_class
|
|
46
|
+
{
|
|
47
|
+
validation: ValidationError,
|
|
48
|
+
out_of_memory: OutOfMemoryError,
|
|
49
|
+
internal: InternalError,
|
|
50
|
+
device_lost: DeviceLostError
|
|
51
|
+
}.fetch(type, Error)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Raises this error as its matching Ruby exception.
|
|
55
|
+
# @return [void]
|
|
56
|
+
# @raise [Error]
|
|
57
|
+
def raise!
|
|
58
|
+
raise exception_class, "GPU error (#{type}): #{message}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Returns a serializable representation of the error.
|
|
62
|
+
# @return [Hash]
|
|
63
|
+
def to_h
|
|
64
|
+
{ type:, message: }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
CompilationMessage = Data.define(
|
|
69
|
+
:type,
|
|
70
|
+
:message,
|
|
71
|
+
:line_num,
|
|
72
|
+
:line_pos,
|
|
73
|
+
:offset,
|
|
74
|
+
:length
|
|
75
|
+
) do
|
|
76
|
+
alias line line_num
|
|
77
|
+
alias column line_pos
|
|
78
|
+
|
|
79
|
+
# Formats the diagnostic with source location, severity, and message.
|
|
80
|
+
# @return [String]
|
|
81
|
+
def to_s
|
|
82
|
+
location = line_num&.positive? ? "#{line_num}:#{line_pos}" : "unknown location"
|
|
83
|
+
"#{location}: #{type}: #{message}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
16
86
|
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,143 @@
|
|
|
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
|
+
# 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
|
|
28
|
+
def initialize(header_path: nil, native: Native)
|
|
29
|
+
@header_path = header_path || self.class.default_header_path
|
|
30
|
+
@native = native
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Verifies the runtime version and Ruby enum ABI against the header.
|
|
34
|
+
# @return [true]
|
|
35
|
+
# @raise [WGPU::Error] when differences are detected
|
|
36
|
+
def verify!
|
|
37
|
+
differences = enum_differences
|
|
38
|
+
version_difference = native_version_difference
|
|
39
|
+
differences.unshift(version_difference) if version_difference
|
|
40
|
+
return true if differences.empty?
|
|
41
|
+
|
|
42
|
+
raise WGPU::Error, "wgpu-native ABI differences:\n#{differences.join("\n")}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Compares Ruby enum values with the pinned native header.
|
|
46
|
+
#
|
|
47
|
+
# @return [Array<String>] human-readable differences
|
|
48
|
+
def enum_differences
|
|
49
|
+
header_enums = parse_header
|
|
50
|
+
ruby_enums.filter_map do |name, mapping|
|
|
51
|
+
header_mapping = header_enums[name]
|
|
52
|
+
next unless header_mapping
|
|
53
|
+
|
|
54
|
+
compare_enum(name, ruby_mapping(mapping), header_mapping)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
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
|
|
61
|
+
def self.default_header_path
|
|
62
|
+
override = ENV["WGPU_HEADER_PATH"]
|
|
63
|
+
return File.expand_path(override) if override && !override.empty?
|
|
64
|
+
|
|
65
|
+
fixture = fixture_header_path
|
|
66
|
+
return fixture if File.file?(fixture)
|
|
67
|
+
|
|
68
|
+
candidates = Distribution.cache_directories.map do |cache_dir|
|
|
69
|
+
File.join(cache_dir, "include", "webgpu", "webgpu.h")
|
|
70
|
+
end
|
|
71
|
+
path = candidates.find { |candidate| File.file?(candidate) }
|
|
72
|
+
return path if path
|
|
73
|
+
|
|
74
|
+
raise WGPU::Error, <<~MSG.chomp
|
|
75
|
+
Pinned #{Distribution::VERSION} webgpu.h was not found.
|
|
76
|
+
Restore the repository ABI fixture, run `bundle exec rake wgpu:install`,
|
|
77
|
+
or set WGPU_HEADER_PATH.
|
|
78
|
+
Searched: #{([fixture] + candidates).join(", ")}
|
|
79
|
+
MSG
|
|
80
|
+
end
|
|
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
|
+
|
|
89
|
+
private
|
|
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
|
+
|
|
100
|
+
def parse_header
|
|
101
|
+
source = File.read(@header_path)
|
|
102
|
+
source.scan(
|
|
103
|
+
/typedef enum WGPU(\w+)\s*\{(.*?)\}\s*WGPU\1(?:\s+WGPU_ENUM_ATTRIBUTE)?;/m
|
|
104
|
+
).to_h do |name, body|
|
|
105
|
+
entries = body.scan(
|
|
106
|
+
/WGPU#{Regexp.escape(name)}_([A-Za-z0-9_]+)\s*=\s*(0x[0-9A-Fa-f]+|\d+)/
|
|
107
|
+
).to_h { |entry, value| [normalize(entry), Integer(value)] }
|
|
108
|
+
entries.delete(FORCE32_KEY)
|
|
109
|
+
[name, entries]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def ruby_enums
|
|
114
|
+
@native.constants(false).sort.filter_map do |constant_name|
|
|
115
|
+
value = @native.const_get(constant_name)
|
|
116
|
+
[constant_name.to_s, value.to_h] if value.is_a?(FFI::Enum)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def ruby_mapping(mapping)
|
|
121
|
+
mapping.to_h { |key, value| [normalize(key), value] }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def normalize(name)
|
|
125
|
+
normalized = name.to_s.delete("_").downcase
|
|
126
|
+
normalized.sub(/\A([123])d/, 'd\1')
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def compare_enum(name, ruby_mapping, header_mapping)
|
|
130
|
+
missing = header_mapping.keys - ruby_mapping.keys
|
|
131
|
+
extra = ruby_mapping.keys - header_mapping.keys - EXTENSION_ENTRIES.fetch(name, [])
|
|
132
|
+
wrong = (header_mapping.keys & ruby_mapping.keys).filter_map do |key|
|
|
133
|
+
next if header_mapping[key] == ruby_mapping[key]
|
|
134
|
+
|
|
135
|
+
"#{key}=#{ruby_mapping[key]} (header #{header_mapping[key]})"
|
|
136
|
+
end
|
|
137
|
+
return if missing.empty? && extra.empty? && wrong.empty?
|
|
138
|
+
|
|
139
|
+
"#{name}: missing=#{missing.inspect}, extra=#{extra.inspect}, mismatched=#{wrong.inspect}"
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -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
|
|
|
@@ -15,12 +21,15 @@ module WGPU
|
|
|
15
21
|
[:pointer, ErrorType, StringView.by_value, :pointer, :pointer], :void
|
|
16
22
|
|
|
17
23
|
callback :device_lost_callback,
|
|
18
|
-
[:pointer,
|
|
24
|
+
[:pointer, DeviceLostReason, StringView.by_value, :pointer, :pointer], :void
|
|
19
25
|
|
|
20
26
|
callback :pop_error_scope_callback,
|
|
21
27
|
[PopErrorScopeStatus, ErrorType, StringView.by_value, :pointer, :pointer], :void
|
|
22
28
|
|
|
23
29
|
callback :queue_work_done_callback,
|
|
24
30
|
[QueueWorkDoneStatus, :pointer, :pointer], :void
|
|
31
|
+
|
|
32
|
+
callback :log_callback,
|
|
33
|
+
[LogLevel, StringView.by_value, :pointer], :void
|
|
25
34
|
end
|
|
26
35
|
end
|
|
@@ -3,12 +3,42 @@
|
|
|
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)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# wgpu-native v27 exports wgpuShaderModuleGetCompilationInfo, but the
|
|
19
|
+
# implementation is a Rust panic stub. Calling it aborts the process, so
|
|
20
|
+
# symbol presence alone cannot be used as a capability check.
|
|
21
|
+
def compilation_info_available?
|
|
22
|
+
Distribution.capability_implemented?(:compilation_info)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Reports whether querying buffer map state is safe in the pinned runtime.
|
|
26
|
+
# @return [Boolean]
|
|
27
|
+
def buffer_map_state_available?
|
|
28
|
+
Distribution.capability_implemented?(:buffer_map_state)
|
|
29
|
+
end
|
|
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]
|
|
39
|
+
def logging_available?
|
|
40
|
+
optional_function_available?(:wgpuSetLogCallback) &&
|
|
41
|
+
optional_function_available?(:wgpuSetLogLevel)
|
|
12
42
|
end
|
|
13
43
|
end
|
|
14
44
|
end
|