zaniah 0.1.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 +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +70 -0
- data/assets/fonts/Abel-Regular.ttf +0 -0
- data/assets/fonts/OFL.txt +93 -0
- data/docs/adr/000-template.md +24 -0
- data/docs/adr/001-native-gpu-backends.md +36 -0
- data/docs/adr/002-objective-c-aggregates.md +34 -0
- data/docs/adr/003-fiddle-call-boundary.md +33 -0
- data/docs/adr/README.md +10 -0
- data/docs/native.md +66 -0
- data/docs/process_pool.md +67 -0
- data/docs/text.md +90 -0
- data/docs/vector_and_list.md +52 -0
- data/examples/ffi_bench.rb +16 -0
- data/examples/linux_smoke.rb +53 -0
- data/examples/mac_event_check.rb +61 -0
- data/examples/mac_poll_bench.rb +24 -0
- data/examples/metal_state_check.rb +72 -0
- data/examples/native_bench.rb +18 -0
- data/examples/native_drop.rb +82 -0
- data/examples/native_font.rb +15 -0
- data/examples/native_ime.rb +50 -0
- data/examples/native_smoke.rb +75 -0
- data/examples/native_text_bench.rb +83 -0
- data/examples/native_watch.rb +25 -0
- data/examples/vulkan_triangle.rb +20 -0
- data/examples/word_count_worker.rb +8 -0
- data/lib/zaniah/anchored.rb +10 -0
- data/lib/zaniah/app.rb +130 -0
- data/lib/zaniah/bounds.rb +22 -0
- data/lib/zaniah/canvas.rb +15 -0
- data/lib/zaniah/color.rb +35 -0
- data/lib/zaniah/configuration.rb +5 -0
- data/lib/zaniah/corners.rb +7 -0
- data/lib/zaniah/data_compat.rb +25 -0
- data/lib/zaniah/device_pixels.rb +7 -0
- data/lib/zaniah/div.rb +5 -0
- data/lib/zaniah/edges.rb +7 -0
- data/lib/zaniah/element.rb +106 -0
- data/lib/zaniah/entity.rb +9 -0
- data/lib/zaniah/entity_context.rb +17 -0
- data/lib/zaniah/error.rb +5 -0
- data/lib/zaniah/ffi/com.rb +24 -0
- data/lib/zaniah/ffi/library.rb +29 -0
- data/lib/zaniah/ffi/objc.rb +86 -0
- data/lib/zaniah/ffi/struct/signature.rb +46 -0
- data/lib/zaniah/ffi/struct.rb +61 -0
- data/lib/zaniah/ffi/wayland.rb +140 -0
- data/lib/zaniah/frame_context.rb +14 -0
- data/lib/zaniah/geometry.rb +16 -0
- data/lib/zaniah/gpu/buffer.rb +23 -0
- data/lib/zaniah/gpu/frame_encoder.rb +43 -0
- data/lib/zaniah/gpu/instance_packing.rb +53 -0
- data/lib/zaniah/gpu/metal.rb +209 -0
- data/lib/zaniah/gpu/open_gl.rb +210 -0
- data/lib/zaniah/gpu/pipeline.rb +7 -0
- data/lib/zaniah/gpu/software.rb +126 -0
- data/lib/zaniah/gpu/texture.rb +33 -0
- data/lib/zaniah/gpu/vulkan.rb +233 -0
- data/lib/zaniah/gpu.rb +18 -0
- data/lib/zaniah/hsla.rb +14 -0
- data/lib/zaniah/image.rb +19 -0
- data/lib/zaniah/input/action_registry.rb +12 -0
- data/lib/zaniah/input/composition.rb +7 -0
- data/lib/zaniah/input/context_predicate.rb +87 -0
- data/lib/zaniah/input/dispatcher.rb +59 -0
- data/lib/zaniah/input/file_drop.rb +7 -0
- data/lib/zaniah/input/focus_handle.rb +24 -0
- data/lib/zaniah/input/key_down.rb +7 -0
- data/lib/zaniah/input/key_up.rb +7 -0
- data/lib/zaniah/input/keymap/binding.rb +9 -0
- data/lib/zaniah/input/keymap.rb +55 -0
- data/lib/zaniah/input/keystroke.rb +20 -0
- data/lib/zaniah/input/mouse_down.rb +7 -0
- data/lib/zaniah/input/mouse_move.rb +7 -0
- data/lib/zaniah/input/mouse_up.rb +7 -0
- data/lib/zaniah/input/scroll_wheel.rb +7 -0
- data/lib/zaniah/input/text_input.rb +7 -0
- data/lib/zaniah/input.rb +17 -0
- data/lib/zaniah/layout/available_space.rb +11 -0
- data/lib/zaniah/layout/engine.rb +214 -0
- data/lib/zaniah/layout/node.rb +36 -0
- data/lib/zaniah/layout/style.rb +19 -0
- data/lib/zaniah/layout/tree.rb +16 -0
- data/lib/zaniah/layout.rb +11 -0
- data/lib/zaniah/length.rb +14 -0
- data/lib/zaniah/length_units.rb +9 -0
- data/lib/zaniah/list/height_index.rb +68 -0
- data/lib/zaniah/list.rb +101 -0
- data/lib/zaniah/overlay.rb +10 -0
- data/lib/zaniah/platform/display.rb +7 -0
- data/lib/zaniah/platform/file_event.rb +7 -0
- data/lib/zaniah/platform/headless/window.rb +185 -0
- data/lib/zaniah/platform/linux/appearance_aware.rb +60 -0
- data/lib/zaniah/platform/linux/displays.rb +55 -0
- data/lib/zaniah/platform/linux/free_type.rb +80 -0
- data/lib/zaniah/platform/linux/watcher.rb +70 -0
- data/lib/zaniah/platform/linux/wayland_display_discovery.rb +60 -0
- data/lib/zaniah/platform/linux/wayland_window.rb +369 -0
- data/lib/zaniah/platform/linux/window.rb +418 -0
- data/lib/zaniah/platform/linux.rb +3 -0
- data/lib/zaniah/platform/mac/app.rb +67 -0
- data/lib/zaniah/platform/mac/core_text.rb +94 -0
- data/lib/zaniah/platform/mac/displays.rb +23 -0
- data/lib/zaniah/platform/mac/watcher.rb +63 -0
- data/lib/zaniah/platform/mac/window.rb +298 -0
- data/lib/zaniah/platform/mac.rb +19 -0
- data/lib/zaniah/platform/tui/input_decoder.rb +145 -0
- data/lib/zaniah/platform/tui/text_renderer.rb +40 -0
- data/lib/zaniah/platform/tui/window.rb +91 -0
- data/lib/zaniah/platform/windows/terminal.rb +127 -0
- data/lib/zaniah/platform/windows/watcher.rb +91 -0
- data/lib/zaniah/platform/windows/window.rb +407 -0
- data/lib/zaniah/platform/windows.rb +3 -0
- data/lib/zaniah/platform.rb +34 -0
- data/lib/zaniah/png.rb +118 -0
- data/lib/zaniah/point.rb +8 -0
- data/lib/zaniah/process_pool/worker.rb +7 -0
- data/lib/zaniah/process_pool.rb +239 -0
- data/lib/zaniah/process_wire.rb +127 -0
- data/lib/zaniah/quad_packer.rb +15 -0
- data/lib/zaniah/scene/sprite_batch.rb +7 -0
- data/lib/zaniah/scene.rb +167 -0
- data/lib/zaniah/scene_renderer.rb +8 -0
- data/lib/zaniah/size.rb +5 -0
- data/lib/zaniah/subscription.rb +14 -0
- data/lib/zaniah/svg/path.rb +134 -0
- data/lib/zaniah/svg/rasterizable.rb +186 -0
- data/lib/zaniah/svg.rb +295 -0
- data/lib/zaniah/task/cancelled.rb +7 -0
- data/lib/zaniah/task/timeout.rb +7 -0
- data/lib/zaniah/task.rb +51 -0
- data/lib/zaniah/task_executor.rb +105 -0
- data/lib/zaniah/text.rb +24 -0
- data/lib/zaniah/text_system/atlas/entry.rb +9 -0
- data/lib/zaniah/text_system/atlas.rb +102 -0
- data/lib/zaniah/text_system/atlas_cache.rb +146 -0
- data/lib/zaniah/text_system/font_db/face.rb +9 -0
- data/lib/zaniah/text_system/font_db.rb +193 -0
- data/lib/zaniah/text_system/glyph.rb +7 -0
- data/lib/zaniah/text_system/line_layout.rb +20 -0
- data/lib/zaniah/text_system/renderer.rb +196 -0
- data/lib/zaniah/text_system/shaper.rb +430 -0
- data/lib/zaniah/text_system/typesetter/copy_error.rb +9 -0
- data/lib/zaniah/text_system/typesetter.rb +168 -0
- data/lib/zaniah/text_system.rb +9 -0
- data/lib/zaniah/unicode.rb +13 -0
- data/lib/zaniah/uniform_list.rb +30 -0
- data/lib/zaniah/version.rb +5 -0
- data/lib/zaniah/view.rb +13 -0
- data/lib/zaniah.rb +29 -0
- data/sig/app.rbs +80 -0
- data/sig/elements.rbs +129 -0
- data/sig/gpu.rbs +108 -0
- data/sig/input.rbs +114 -0
- data/sig/layout.rbs +53 -0
- data/sig/native.rbs +88 -0
- data/sig/platform.rbs +94 -0
- data/sig/scene.rbs +52 -0
- data/sig/shaper.rbs +8 -0
- data/sig/text.rbs +137 -0
- data/sig/zaniah.rbs +120 -0
- data/tools/check_dependencies.rb +52 -0
- metadata +249 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "library"
|
|
4
|
+
|
|
5
|
+
module Zaniah
|
|
6
|
+
module FFI
|
|
7
|
+
# Runtime protocol metadata replaces generated C bindings. Core interfaces
|
|
8
|
+
# are exported by libwayland-client; these extensions use protocol v1.
|
|
9
|
+
class Wayland
|
|
10
|
+
P, I, U, V = Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_UINT, Fiddle::TYPE_VOID
|
|
11
|
+
PROTOCOLS = {
|
|
12
|
+
"xdg_wm_base" => [[
|
|
13
|
+
["destroy", ""], ["create_positioner", "n", "xdg_positioner"],
|
|
14
|
+
["get_xdg_surface", "no", "xdg_surface", "wl_surface"], ["pong", "u"]
|
|
15
|
+
], [["ping", "u"]]],
|
|
16
|
+
"xdg_positioner" => [[], []], "xdg_popup" => [[], []],
|
|
17
|
+
"xdg_surface" => [[
|
|
18
|
+
["destroy", ""], ["get_toplevel", "n", "xdg_toplevel"],
|
|
19
|
+
["get_popup", "n?oo", "xdg_popup", "xdg_surface", "xdg_positioner"],
|
|
20
|
+
["set_window_geometry", "iiii"], ["ack_configure", "u"]
|
|
21
|
+
], [["configure", "u"]]],
|
|
22
|
+
"xdg_toplevel" => [[
|
|
23
|
+
["destroy", ""], ["set_parent", "?o", "xdg_toplevel"], ["set_title", "s"], ["set_app_id", "s"],
|
|
24
|
+
["show_window_menu", "ouii", "wl_seat"], ["move", "ou", "wl_seat"], ["resize", "ouu", "wl_seat"],
|
|
25
|
+
["set_max_size", "ii"], ["set_min_size", "ii"], ["set_maximized", ""], ["unset_maximized", ""],
|
|
26
|
+
["set_fullscreen", "?o", "wl_output"], ["unset_fullscreen", ""], ["set_minimized", ""]
|
|
27
|
+
], [["configure", "iia"], ["close", ""]]],
|
|
28
|
+
"zwp_text_input_manager_v3" => [[["destroy", ""], ["get_text_input", "no", "zwp_text_input_v3", "wl_seat"]], []],
|
|
29
|
+
"zwp_text_input_v3" => [[
|
|
30
|
+
["destroy", ""], ["enable", ""], ["disable", ""], ["set_surrounding_text", "sii"],
|
|
31
|
+
["set_text_change_cause", "u"], ["set_content_type", "uu"], ["set_cursor_rectangle", "iiii"], ["commit", ""]
|
|
32
|
+
], [["enter", "o", "wl_surface"], ["leave", "o", "wl_surface"], ["preedit_string", "?sii"],
|
|
33
|
+
["commit_string", "?s"], ["delete_surrounding_text", "uu"], ["done", "u"]]],
|
|
34
|
+
"zxdg_decoration_manager_v1" => [[["destroy", ""], ["get_toplevel_decoration", "no", "zxdg_toplevel_decoration_v1", "xdg_toplevel"]], []],
|
|
35
|
+
"zxdg_toplevel_decoration_v1" => [[["destroy", ""], ["set_mode", "u"], ["unset_mode", ""]], [["configure", "u"]]],
|
|
36
|
+
"zxdg_output_manager_v1" => [[["destroy", ""], ["get_xdg_output", "no", "zxdg_output_v1", "wl_output"]], []],
|
|
37
|
+
"zxdg_output_v1" => [[["destroy", ""]], [["logical_position", "ii"], ["logical_size", "ii"], ["done", ""], ["name", "2s"], ["description", "2s"]]]
|
|
38
|
+
}.freeze
|
|
39
|
+
PROTOCOL_VERSIONS = {"zxdg_output_manager_v1" => 3, "zxdg_output_v1" => 3}.freeze
|
|
40
|
+
attr_reader :library, :display
|
|
41
|
+
|
|
42
|
+
def initialize
|
|
43
|
+
@library = Library.new("libwayland-client.so.0")
|
|
44
|
+
@interfaces, @keep, @listeners, @objects = {}, [], [], []
|
|
45
|
+
PROTOCOLS.each_key { |name| @interfaces[name] = Fiddle::Pointer.malloc(40, Fiddle::RUBY_FREE) }
|
|
46
|
+
PROTOCOLS.each do |name, (requests, events)|
|
|
47
|
+
bytes = [keep_string(name), PROTOCOL_VERSIONS.fetch(name, 1), requests.length, messages(requests), events.length, messages(events)].pack("JiiJi x4J")
|
|
48
|
+
@interfaces[name][0, 40] = bytes
|
|
49
|
+
end
|
|
50
|
+
@display = @library.fn(:wl_display_connect, [P], P).call(0)
|
|
51
|
+
raise Zaniah::Error, "cannot connect to WAYLAND_DISPLAY" if @display.null?
|
|
52
|
+
end
|
|
53
|
+
def interface(name) = @interfaces[name] ||= Fiddle::Pointer.new(@library.handle["#{name}_interface"])
|
|
54
|
+
def keep_string(text)
|
|
55
|
+
string = text + "\0"
|
|
56
|
+
@keep << string
|
|
57
|
+
Fiddle::Pointer[string].to_i
|
|
58
|
+
end
|
|
59
|
+
def messages(definitions)
|
|
60
|
+
return 0 if definitions.empty?
|
|
61
|
+
data = definitions.map do |name, signature, *interfaces|
|
|
62
|
+
count = signature.delete("?0123456789").length
|
|
63
|
+
types = Array.new(count, 0)
|
|
64
|
+
interfaces.each_with_index { |type, i| types[i] = interface(type).to_i if type }
|
|
65
|
+
packed = types.pack("J*")
|
|
66
|
+
@keep << packed
|
|
67
|
+
[keep_string(name), keep_string(signature), Fiddle::Pointer[packed].to_i].pack("J3")
|
|
68
|
+
end.join
|
|
69
|
+
@keep << data
|
|
70
|
+
Fiddle::Pointer[data].to_i
|
|
71
|
+
end
|
|
72
|
+
def version(proxy) = @library.fn(:wl_proxy_get_version, [P], U).call(proxy)
|
|
73
|
+
def request(proxy, opcode, *arguments, new_interface: nil, version: nil, destroy: false)
|
|
74
|
+
strings = []
|
|
75
|
+
values = arguments.map do |value|
|
|
76
|
+
if value.is_a?(String)
|
|
77
|
+
strings << value + "\0"
|
|
78
|
+
Fiddle::Pointer[strings.last].to_i
|
|
79
|
+
else
|
|
80
|
+
value.to_i & 0xffffffffffffffff
|
|
81
|
+
end
|
|
82
|
+
end.pack("Q*")
|
|
83
|
+
object = @library.fn(:wl_proxy_marshal_array_flags, [P, U, P, U, U, P], P).call(proxy, opcode,
|
|
84
|
+
new_interface ? interface(new_interface) : 0, version || self.version(proxy), destroy ? 1 : 0, values.empty? ? 0 : values)
|
|
85
|
+
@objects << object if new_interface && !object.null?
|
|
86
|
+
@objects.delete(proxy) if destroy
|
|
87
|
+
object
|
|
88
|
+
end
|
|
89
|
+
def listen(proxy, events)
|
|
90
|
+
@objects << proxy unless @objects.include?(proxy)
|
|
91
|
+
callbacks = events.map do |types, block|
|
|
92
|
+
Fiddle::Closure::BlockCaller.new(V, [P, P, *types]) do |_data, object, *arguments|
|
|
93
|
+
begin
|
|
94
|
+
block&.call(object, *arguments)
|
|
95
|
+
rescue StandardError => error
|
|
96
|
+
@error = error
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
table = callbacks.map(&:to_i).pack("J*")
|
|
101
|
+
result = @library.fn(:wl_proxy_add_listener, [P, P, P], I).call(proxy, table, 0)
|
|
102
|
+
raise Zaniah::Error, "Wayland listener registration failed" unless result.zero?
|
|
103
|
+
@listeners << [table, callbacks]
|
|
104
|
+
end
|
|
105
|
+
def roundtrip
|
|
106
|
+
result = @library.fn(:wl_display_roundtrip, [P], I).call(@display)
|
|
107
|
+
raise @error if @error
|
|
108
|
+
raise Zaniah::Error, "Wayland compositor disconnected" if result.negative?
|
|
109
|
+
end
|
|
110
|
+
def poll(timeout: 0)
|
|
111
|
+
until @library.fn(:wl_display_prepare_read, [P], I).call(@display).zero?
|
|
112
|
+
result = @library.fn(:wl_display_dispatch_pending, [P], I).call(@display)
|
|
113
|
+
raise @error if @error
|
|
114
|
+
raise Zaniah::Error, "Wayland compositor disconnected" if result.negative?
|
|
115
|
+
end
|
|
116
|
+
@library.fn(:wl_display_flush, [P], I).call(@display)
|
|
117
|
+
fd = @library.fn(:wl_display_get_fd, [P], I).call(@display)
|
|
118
|
+
ready = IO.select([IO.for_fd(fd, autoclose: false)], nil, nil, timeout)
|
|
119
|
+
if ready
|
|
120
|
+
result = @library.fn(:wl_display_read_events, [P], I).call(@display)
|
|
121
|
+
raise Zaniah::Error, "Wayland compositor disconnected" if result.negative?
|
|
122
|
+
else
|
|
123
|
+
@library.fn(:wl_display_cancel_read, [P], V).call(@display)
|
|
124
|
+
end
|
|
125
|
+
@library.fn(:wl_display_dispatch_pending, [P], I).call(@display)
|
|
126
|
+
raise @error if @error
|
|
127
|
+
end
|
|
128
|
+
def destroy(proxy)
|
|
129
|
+
return unless proxy && !proxy.to_i.zero?
|
|
130
|
+
@library.fn(:wl_proxy_destroy, [P], V).call(proxy)
|
|
131
|
+
@objects.delete(proxy)
|
|
132
|
+
end
|
|
133
|
+
def close
|
|
134
|
+
@objects.reverse_each { |object| @library.fn(:wl_proxy_destroy, [P], V).call(object) }
|
|
135
|
+
@objects.clear
|
|
136
|
+
@library.fn(:wl_display_disconnect, [P], V).call(@display)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zaniah
|
|
4
|
+
class FrameContext
|
|
5
|
+
attr_reader :window, :scene, :dispatcher, :text_system
|
|
6
|
+
|
|
7
|
+
def initialize(window)
|
|
8
|
+
@window, @scene = window, window.scene
|
|
9
|
+
@dispatcher, @text_system = window.dispatcher, window.text_system
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def state(key, &initial) = @window.element_state(key, &initial)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "point"
|
|
4
|
+
require_relative "size"
|
|
5
|
+
require_relative "edges"
|
|
6
|
+
require_relative "corners"
|
|
7
|
+
require_relative "bounds"
|
|
8
|
+
require_relative "length"
|
|
9
|
+
require_relative "device_pixels"
|
|
10
|
+
require_relative "length_units"
|
|
11
|
+
require_relative "color"
|
|
12
|
+
require_relative "hsla"
|
|
13
|
+
|
|
14
|
+
module Zaniah
|
|
15
|
+
extend LengthUnits
|
|
16
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zaniah
|
|
4
|
+
module GPU
|
|
5
|
+
class Buffer
|
|
6
|
+
attr_reader :size, :usage, :data
|
|
7
|
+
|
|
8
|
+
def initialize(size, usage: :vertex, mutable: true)
|
|
9
|
+
raise ArgumentError, "invalid buffer size" unless size.is_a?(Integer) && size >= 0
|
|
10
|
+
@size, @usage, @mutable = size, usage, mutable
|
|
11
|
+
@data = "\0".b * size
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def write(bytes, offset: 0)
|
|
15
|
+
raise ArgumentError, "buffer write out of bounds" if offset.negative? || offset + bytes.bytesize > @size
|
|
16
|
+
@data[offset, bytes.bytesize] = bytes
|
|
17
|
+
self
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def release = @data.clear
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zaniah
|
|
4
|
+
module GPU
|
|
5
|
+
class FrameEncoder
|
|
6
|
+
def initialize(device, clear)
|
|
7
|
+
@device, @clear, @scene, @textures = device, clear, Scene.new, {}
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def set_pipeline(pipeline) = @pipeline = pipeline
|
|
11
|
+
def set_uniforms(**values) = @uniforms = values
|
|
12
|
+
def set_texture(slot, texture) = @textures[slot] = texture
|
|
13
|
+
def set_scissor(bounds) = @scissor = bounds
|
|
14
|
+
|
|
15
|
+
def draw_instanced(vertex_count:, instance_buffer:, instance_count:)
|
|
16
|
+
raise ArgumentError, "expected quad vertices" unless vertex_count == 4
|
|
17
|
+
raise ArgumentError, "pipeline not set" unless @pipeline
|
|
18
|
+
draw = lambda do
|
|
19
|
+
values = instance_buffer.data.unpack("f*")
|
|
20
|
+
if @pipeline.shader == :quad
|
|
21
|
+
raise ArgumentError, "instance buffer too small" if values.length < instance_count * Scene::QUAD_STRIDE
|
|
22
|
+
instance_count.times do |index|
|
|
23
|
+
quad = values.slice(index * Scene::QUAD_STRIDE, Scene::QUAD_STRIDE)
|
|
24
|
+
@scene.quad(*quad.first(4), color: quad[4, 4], radius: quad[8, 4], border_width: quad[12], border_color: quad[13, 4])
|
|
25
|
+
end
|
|
26
|
+
elsif [:mono_sprite, :poly_sprite].include?(@pipeline.shader)
|
|
27
|
+
raise ArgumentError, "instance buffer too small" if values.length < instance_count * 12
|
|
28
|
+
texture = @textures.fetch(0)
|
|
29
|
+
instance_count.times do |index|
|
|
30
|
+
sprite = values.slice(index * 12, 12)
|
|
31
|
+
@scene.sprite(*sprite.first(4), texture: texture, color: sprite[4, 4], source: Bounds.new(*sprite[8, 4]))
|
|
32
|
+
end
|
|
33
|
+
else
|
|
34
|
+
raise ArgumentError, "unsupported instanced shader #{@pipeline.shader}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
@scissor ? @scene.clip(@scissor, &draw) : draw.call
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def present = @device.render(@scene, clear: @clear)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zaniah
|
|
4
|
+
module GPU
|
|
5
|
+
# Consecutive commands only are batched, preserving alpha and layer order.
|
|
6
|
+
module InstancePacking
|
|
7
|
+
STRIDE = 24
|
|
8
|
+
def self.pack(scene)
|
|
9
|
+
data, batches, bytes = [], [], "".b
|
|
10
|
+
scene.each_command do |kind, offset, clip|
|
|
11
|
+
texture = nil
|
|
12
|
+
first = bytes.bytesize / (STRIDE * 4) + data.length / STRIDE
|
|
13
|
+
count = 1
|
|
14
|
+
case kind
|
|
15
|
+
when :quad
|
|
16
|
+
q, i = scene.quads, offset
|
|
17
|
+
data.push(q[i], q[i+1], q[i+2], q[i+3], q[i+4], q[i+5], q[i+6], q[i+7],
|
|
18
|
+
q[i+8], q[i+9], q[i+10], q[i+11], q[i+13], q[i+14], q[i+15], q[i+16], q[i+12], 0, 0, 0, 0, 0, 0, 0)
|
|
19
|
+
when :sprite
|
|
20
|
+
s, i = scene.sprite_data, offset
|
|
21
|
+
texture = scene.textures.fetch(s[i+8])
|
|
22
|
+
data.push(s[i], s[i+1], s[i+2], s[i+3], s[i+4], s[i+5], s[i+6], s[i+7],
|
|
23
|
+
0, 0, 0, 0, 0, 0, 0, 0, 0, texture.format == :r8 ? 1 : 2, 0, 0,
|
|
24
|
+
s[i+9].to_f / texture.width, s[i+10].to_f / texture.height,
|
|
25
|
+
s[i+11].to_f / texture.width, s[i+12].to_f / texture.height)
|
|
26
|
+
when :sprite_batch
|
|
27
|
+
unless data.empty?
|
|
28
|
+
bytes << data.pack("f*")
|
|
29
|
+
data.clear
|
|
30
|
+
end
|
|
31
|
+
batch = scene.sprite_batches.fetch(offset)
|
|
32
|
+
bytes << batch.bytes
|
|
33
|
+
kind, texture = :sprite, batch.texture
|
|
34
|
+
count = batch.bytes.bytesize / (STRIDE * 4)
|
|
35
|
+
when :triangle
|
|
36
|
+
p, i = scene.paths, offset
|
|
37
|
+
data.push(p[i], p[i+1], p[i+2], p[i+3], p[i+6], p[i+7], p[i+8], p[i+9],
|
|
38
|
+
p[i+4], p[i+5], 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0)
|
|
39
|
+
else raise ArgumentError, "unsupported primitive #{kind}"
|
|
40
|
+
end
|
|
41
|
+
previous = batches.last&.first
|
|
42
|
+
if previous && previous[0] == kind && previous[1].equal?(texture) && previous[2] == clip
|
|
43
|
+
batches.last[2] += count
|
|
44
|
+
else
|
|
45
|
+
batches << [[kind, texture, clip], first, count]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
bytes << data.pack("f*") unless data.empty?
|
|
49
|
+
[bytes, batches]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "instance_packing"
|
|
4
|
+
require_relative "../ffi/objc"
|
|
5
|
+
|
|
6
|
+
module Zaniah
|
|
7
|
+
module GPU
|
|
8
|
+
class Metal
|
|
9
|
+
O = FFI::ObjC
|
|
10
|
+
LIB = FFI::Library.new("/System/Library/Frameworks/Metal.framework/Metal")
|
|
11
|
+
POINTER_ARGS = [:pointer].freeze
|
|
12
|
+
ULONG_ARGS = [:ulong].freeze
|
|
13
|
+
BUFFER_ARGS = [:pointer, :ulong, :ulong].freeze
|
|
14
|
+
TEXTURE_ARGS = [:pointer, :ulong].freeze
|
|
15
|
+
DRAW_ARGS = [:ulong, :ulong, :ulong, :ulong].freeze
|
|
16
|
+
CLEAR_ARGS = [:clear_color].freeze
|
|
17
|
+
attr_reader :width, :height, :draw_calls
|
|
18
|
+
SOURCE = <<~MSL.freeze
|
|
19
|
+
#include <metal_stdlib>
|
|
20
|
+
using namespace metal;
|
|
21
|
+
struct Vertex { float4 position [[position]]; float2 local; float2 size;
|
|
22
|
+
float4 color; float4 radii; float4 border; float4 info; float2 uv; };
|
|
23
|
+
vertex Vertex zaniah_vertex(uint v [[vertex_id]], uint instance [[instance_id]],
|
|
24
|
+
const device float *data [[buffer(0)]], constant float2 &viewport [[buffer(1)]]) {
|
|
25
|
+
const device float *p = data + instance * 24;
|
|
26
|
+
float2 corners[4] = {float2(0,0),float2(1,0),float2(0,1),float2(1,1)};
|
|
27
|
+
float2 corner = corners[v], size = float2(p[2],p[3]);
|
|
28
|
+
float2 position = float2(p[0],p[1]) + corner * size;
|
|
29
|
+
if (p[17] == 3) position = v == 0 ? float2(p[0],p[1]) : v == 1 ? float2(p[2],p[3]) : float2(p[8],p[9]);
|
|
30
|
+
Vertex out;
|
|
31
|
+
out.position = float4(position.x / viewport.x * 2 - 1, 1 - position.y / viewport.y * 2, 0, 1);
|
|
32
|
+
out.local = corner * size; out.size = size;
|
|
33
|
+
out.color = float4(p[4],p[5],p[6],p[7]); out.radii = float4(p[8],p[9],p[10],p[11]);
|
|
34
|
+
out.border = float4(p[12],p[13],p[14],p[15]); out.info = float4(p[16],p[17],p[18],p[19]);
|
|
35
|
+
out.uv = float2(p[20],p[21]) + corner * float2(p[22],p[23]); return out;
|
|
36
|
+
}
|
|
37
|
+
fragment float4 zaniah_fragment(Vertex in [[stage_in]], texture2d<float> atlas [[texture(0)]]) {
|
|
38
|
+
float4 color = in.color;
|
|
39
|
+
if (in.info.y == 0) {
|
|
40
|
+
float radius = in.local.y < in.size.y/2 ? (in.local.x < in.size.x/2 ? in.radii.x : in.radii.y) : (in.local.x < in.size.x/2 ? in.radii.w : in.radii.z);
|
|
41
|
+
radius = clamp(radius, 0.0f, min(in.size.x,in.size.y)/2);
|
|
42
|
+
float2 q = abs(in.local - in.size/2) - in.size/2 + radius;
|
|
43
|
+
float distance = length(max(q,0.0f)) + min(max(q.x,q.y),0.0f) - radius;
|
|
44
|
+
float coverage = clamp(0.5f - distance, 0.0f, 1.0f);
|
|
45
|
+
if (in.info.x > 0 && distance >= -in.info.x) color = in.border;
|
|
46
|
+
color.a *= coverage;
|
|
47
|
+
} else if (in.info.y == 1 || in.info.y == 2) {
|
|
48
|
+
constexpr sampler texture_sampler(coord::normalized, address::clamp_to_edge, filter::nearest);
|
|
49
|
+
float4 sample = atlas.sample(texture_sampler,in.uv);
|
|
50
|
+
if (in.info.y == 1) color.a *= sample.r; else color *= sample;
|
|
51
|
+
}
|
|
52
|
+
return float4(color.rgb * color.a, color.a);
|
|
53
|
+
}
|
|
54
|
+
MSL
|
|
55
|
+
|
|
56
|
+
def initialize(window)
|
|
57
|
+
@window, @textures = window, {}
|
|
58
|
+
@device = LIB.fn(:MTLCreateSystemDefaultDevice, [], Fiddle::TYPE_VOIDP).call.to_i
|
|
59
|
+
raise Error, "Metal device unavailable" if @device.zero?
|
|
60
|
+
@queue = O.send(@device, "newCommandQueue")
|
|
61
|
+
O.send(window.layer, "setDevice:", @device, args: [:pointer], result: :void)
|
|
62
|
+
O.send(window.layer, "setPixelFormat:", 80, args: [:ulong], result: :void)
|
|
63
|
+
O.send(window.layer, "setFramebufferOnly:", 0, args: [:bool], result: :void)
|
|
64
|
+
@pipeline = compile_pipeline(SOURCE)
|
|
65
|
+
# Metal render-pass descriptors are reusable; only the drawable target
|
|
66
|
+
# and a changed clear color need updating between encoders.
|
|
67
|
+
@render_pass = O.new("MTLRenderPassDescriptor")
|
|
68
|
+
@color_attachment = O.send(O.send(@render_pass, "colorAttachments"), "objectAtIndexedSubscript:", 0, args: ULONG_ARGS)
|
|
69
|
+
O.send(@color_attachment, "setLoadAction:", 2, args: ULONG_ARGS, result: :void)
|
|
70
|
+
O.send(@color_attachment, "setStoreAction:", 1, args: ULONG_ARGS, result: :void)
|
|
71
|
+
@clear_input = Object.new
|
|
72
|
+
@scissor = Array.new(4, 0)
|
|
73
|
+
@scissor_signature = FFI::Struct::Signature.new([:pointer, :pointer, :scissor], :void)
|
|
74
|
+
@scissor_selector = O.selector("setScissorRect:")
|
|
75
|
+
@white = create_texture(1, 1, data: "\xff".b * 4)
|
|
76
|
+
resize(window.content_size.width, window.content_size.height)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def compile_pipeline(source)
|
|
80
|
+
error = Fiddle::Pointer.malloc(Fiddle::SIZEOF_VOIDP, Fiddle::RUBY_FREE)
|
|
81
|
+
error[0, Fiddle::SIZEOF_VOIDP] = [0].pack("J")
|
|
82
|
+
library = O.send(@device, "newLibraryWithSource:options:error:", O.string(source), 0, error,
|
|
83
|
+
args: [:pointer] * 3)
|
|
84
|
+
raise Error, native_error(error, "Metal shader compilation failed") if library.zero?
|
|
85
|
+
vertex = O.send(library, "newFunctionWithName:", O.string("zaniah_vertex"), args: [:pointer])
|
|
86
|
+
fragment = O.send(library, "newFunctionWithName:", O.string("zaniah_fragment"), args: [:pointer])
|
|
87
|
+
descriptor = O.new("MTLRenderPipelineDescriptor")
|
|
88
|
+
O.send(descriptor, "setVertexFunction:", vertex, args: [:pointer], result: :void)
|
|
89
|
+
O.send(descriptor, "setFragmentFunction:", fragment, args: [:pointer], result: :void)
|
|
90
|
+
attachment = O.send(O.send(descriptor, "colorAttachments"), "objectAtIndexedSubscript:", 0, args: [:ulong])
|
|
91
|
+
O.send(attachment, "setPixelFormat:", 80, args: [:ulong], result: :void)
|
|
92
|
+
O.send(attachment, "setBlendingEnabled:", 1, args: [:bool], result: :void)
|
|
93
|
+
{"setSourceRGBBlendFactor:" => 1, "setSourceAlphaBlendFactor:" => 1,
|
|
94
|
+
"setDestinationRGBBlendFactor:" => 5, "setDestinationAlphaBlendFactor:" => 5}.each do |name, value|
|
|
95
|
+
O.send(attachment, name, value, args: [:ulong], result: :void)
|
|
96
|
+
end
|
|
97
|
+
pipeline = O.send(@device, "newRenderPipelineStateWithDescriptor:error:", descriptor, error, args: [:pointer, :pointer])
|
|
98
|
+
raise Error, native_error(error, "Metal pipeline creation failed") if pipeline.zero?
|
|
99
|
+
pipeline
|
|
100
|
+
ensure
|
|
101
|
+
[descriptor, vertex, fragment, library].compact.each { |object| O.release(object) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def resize(width, height)
|
|
105
|
+
raise ArgumentError, "invalid viewport" unless width.positive? && height.positive?
|
|
106
|
+
@width, @height = width.to_i, height.to_i
|
|
107
|
+
@scale = @window.scale_factor
|
|
108
|
+
@pixel_width, @pixel_height = (width * @scale).round, (height * @scale).round
|
|
109
|
+
@viewport_bytes = [@width, @height].pack("f2")
|
|
110
|
+
@viewport_pointer = Fiddle::Pointer[@viewport_bytes]
|
|
111
|
+
@full_scissor = [0, 0, @pixel_width, @pixel_height].freeze
|
|
112
|
+
O.send(@window.layer, "setContentsScale:", @scale, args: [:double], result: :void)
|
|
113
|
+
O.send(@window.layer, "setDrawableSize:", [@pixel_width, @pixel_height], args: [:size], result: :void)
|
|
114
|
+
end
|
|
115
|
+
def create_texture(width, height, **options) = Texture.new(width, height, **options)
|
|
116
|
+
def create_buffer(size, **options) = Buffer.new(size, **options)
|
|
117
|
+
def create_pipeline(shader:, blend: :premultiplied) = Pipeline.new(shader, blend)
|
|
118
|
+
def begin_frame(clear: "#0000") = FrameEncoder.new(self, clear)
|
|
119
|
+
|
|
120
|
+
def render(scene, clear: "#0000")
|
|
121
|
+
pool = O.new("NSAutoreleasePool")
|
|
122
|
+
@draw_calls = 0
|
|
123
|
+
drawable = O.send(@window.layer, "nextDrawable")
|
|
124
|
+
return if drawable.zero?
|
|
125
|
+
target = O.send(drawable, "texture")
|
|
126
|
+
O.release(@last_texture) if @last_texture
|
|
127
|
+
@last_texture = O.send(target, "retain")
|
|
128
|
+
command = O.send(@queue, "commandBuffer")
|
|
129
|
+
O.send(@color_attachment, "setTexture:", target, args: POINTER_ARGS, result: :void)
|
|
130
|
+
unless clear == @clear_input
|
|
131
|
+
O.send(@color_attachment, "setClearColor:", Color.parse(clear).premultiplied, args: CLEAR_ARGS, result: :void)
|
|
132
|
+
@clear_input = clear.is_a?(String) || clear.is_a?(Array) ? clear.dup.freeze : clear
|
|
133
|
+
end
|
|
134
|
+
encoder = O.send(command, "renderCommandEncoderWithDescriptor:", @render_pass, args: POINTER_ARGS)
|
|
135
|
+
O.send(encoder, "setRenderPipelineState:", @pipeline, args: POINTER_ARGS, result: :void)
|
|
136
|
+
bytes, batches = InstancePacking.pack(scene)
|
|
137
|
+
unless bytes.empty?
|
|
138
|
+
buffer = O.send(@device, "newBufferWithBytes:length:options:", Fiddle::Pointer[bytes], bytes.bytesize, 0, args: BUFFER_ARGS)
|
|
139
|
+
O.send(encoder, "setVertexBytes:length:atIndex:", @viewport_pointer, @viewport_bytes.bytesize, 1, args: BUFFER_ARGS, result: :void)
|
|
140
|
+
batches.each do |(kind, texture, clip), first, count|
|
|
141
|
+
scissor = @full_scissor
|
|
142
|
+
if clip
|
|
143
|
+
left, top = [clip.x, 0].max, [clip.y, 0].max
|
|
144
|
+
width, height = [clip.right, @width].min - left, [clip.bottom, @height].min - top
|
|
145
|
+
next unless width.positive? && height.positive?
|
|
146
|
+
scissor = @scissor
|
|
147
|
+
scissor[0], scissor[1] = (left * @scale).to_i, (top * @scale).to_i
|
|
148
|
+
scissor[2], scissor[3] = (width * @scale).to_i, (height * @scale).to_i
|
|
149
|
+
end
|
|
150
|
+
next if scissor[2].zero? || scissor[3].zero?
|
|
151
|
+
# MTLScissorRect is four NSUInteger values, the same layout as two ranges.
|
|
152
|
+
@scissor_signature.call(O::MESSAGE_SEND, encoder, @scissor_selector, scissor)
|
|
153
|
+
O.send(encoder, "setVertexBuffer:offset:atIndex:", buffer, first * InstancePacking::STRIDE * 4, 0, args: BUFFER_ARGS, result: :void)
|
|
154
|
+
O.send(encoder, "setFragmentTexture:atIndex:", native_texture(texture || @white), 0, args: TEXTURE_ARGS, result: :void)
|
|
155
|
+
O.send(encoder, "drawPrimitives:vertexStart:vertexCount:instanceCount:", kind == :triangle ? 3 : 4, 0, kind == :triangle ? 3 : 4, count, args: DRAW_ARGS, result: :void)
|
|
156
|
+
@draw_calls += 1
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
O.send(encoder, "endEncoding", result: :void)
|
|
160
|
+
O.send(command, "presentDrawable:", drawable, args: POINTER_ARGS, result: :void)
|
|
161
|
+
O.send(command, "commit", result: :void)
|
|
162
|
+
O.release(@last_command) if @last_command
|
|
163
|
+
@last_command = O.send(command, "retain")
|
|
164
|
+
ensure
|
|
165
|
+
O.release(buffer) if buffer
|
|
166
|
+
O.release(pool) if pool
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def native_texture(texture)
|
|
170
|
+
entry = @textures[texture]
|
|
171
|
+
unless entry
|
|
172
|
+
descriptor = O.send(O.klass("MTLTextureDescriptor"), "texture2DDescriptorWithPixelFormat:width:height:mipmapped:", texture.format == :r8 ? 10 : 70, texture.width, texture.height, 0, args: [:ulong, :ulong, :ulong, :bool])
|
|
173
|
+
O.send(descriptor, "setStorageMode:", 0, args: [:ulong], result: :void)
|
|
174
|
+
entry = @textures[texture] = [O.send(@device, "newTextureWithDescriptor:", descriptor, args: [:pointer]), nil]
|
|
175
|
+
raise Error, "Metal texture allocation failed" if entry[0].zero?
|
|
176
|
+
end
|
|
177
|
+
digest = texture.revision
|
|
178
|
+
if entry[1] != digest
|
|
179
|
+
region = [0, 0, 0, texture.width, texture.height, 1]
|
|
180
|
+
O.send(entry[0], "replaceRegion:mipmapLevel:withBytes:bytesPerRow:", region, 0, Fiddle::Pointer[texture.data], texture.width * (texture.format == :r8 ? 1 : 4), args: [:region, :ulong, :pointer, :ulong], result: :void)
|
|
181
|
+
entry[1] = digest
|
|
182
|
+
end
|
|
183
|
+
entry[0]
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def pixels
|
|
187
|
+
return "\0".b * (@pixel_width * @pixel_height * 4) unless @last_texture
|
|
188
|
+
O.send(@last_command, "waitUntilCompleted", result: :void)
|
|
189
|
+
status = O.send(@last_command, "status", result: :ulong)
|
|
190
|
+
raise Error, O.text(O.send(O.send(@last_command, "error"), "localizedDescription")) if status == 5
|
|
191
|
+
bytes = "\0".b * (@pixel_width * @pixel_height * 4)
|
|
192
|
+
O.send(@last_texture, "getBytes:bytesPerRow:fromRegion:mipmapLevel:", Fiddle::Pointer[bytes], @pixel_width * 4, [0, 0, 0, @pixel_width, @pixel_height, 1], 0, args: [:pointer, :ulong, :region, :ulong], result: :void)
|
|
193
|
+
bytes.unpack("C*").each_slice(4).flat_map { |b, g, r, a| a.zero? ? [0, 0, 0, 0] : [(r * 255 / a).clamp(0, 255), (g * 255 / a).clamp(0, 255), (b * 255 / a).clamp(0, 255), a] }.pack("C*")
|
|
194
|
+
end
|
|
195
|
+
def write_png(path) = PNG.write(path, @pixel_width, @pixel_height, pixels)
|
|
196
|
+
def release
|
|
197
|
+
O.send(@last_command, "waitUntilCompleted", result: :void) if @last_command
|
|
198
|
+
@textures.each_value { |entry| O.release(entry[0]) }
|
|
199
|
+
@textures.clear
|
|
200
|
+
[@last_command, @last_texture, @render_pass, @pipeline, @queue, @device].compact.each { |object| O.release(object) }
|
|
201
|
+
@last_command = @last_texture = @render_pass = nil
|
|
202
|
+
end
|
|
203
|
+
def native_error(pointer, fallback)
|
|
204
|
+
object = pointer[0, Fiddle::SIZEOF_VOIDP].unpack1("J")
|
|
205
|
+
object.zero? ? fallback : O.text(O.send(object, "localizedDescription"))
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|