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,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tmpdir"
|
|
4
|
+
require "rbconfig"
|
|
5
|
+
|
|
6
|
+
Dir.mktmpdir("zaniah-linux-") do |directory|
|
|
7
|
+
log = File.open(File.join(directory, "display.log"), "w")
|
|
8
|
+
ENV["LIBGL_ALWAYS_SOFTWARE"] = "1"
|
|
9
|
+
wayland = ARGV.include?("--wayland")
|
|
10
|
+
if wayland
|
|
11
|
+
ENV["XDG_RUNTIME_DIR"] = directory
|
|
12
|
+
ENV["WAYLAND_DISPLAY"] = "wayland-zaniah"
|
|
13
|
+
ENV.delete("DISPLAY")
|
|
14
|
+
pid = Process.spawn("weston", "--backend=headless-backend.so", "--renderer=gl", "--socket=wayland-zaniah", "--idle-time=0", out: log, err: log)
|
|
15
|
+
socket = File.join(directory, "wayland-zaniah")
|
|
16
|
+
else
|
|
17
|
+
ENV["ZANIAH_CHECK_INPUT"] = "1"
|
|
18
|
+
ENV["ZANIAH_CHECK_DPI"] = "1"
|
|
19
|
+
ENV.delete("WAYLAND_DISPLAY")
|
|
20
|
+
number = 90 + Process.pid % 100
|
|
21
|
+
ENV["DISPLAY"] = ":#{number}"
|
|
22
|
+
pid = Process.spawn("Xvfb", ENV["DISPLAY"], "-screen", "0", "1280x800x24", "+extension", "GLX", out: log, err: log)
|
|
23
|
+
socket = "/tmp/.X11-unix/X#{number}"
|
|
24
|
+
end
|
|
25
|
+
begin
|
|
26
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 10
|
|
27
|
+
until File.socket?(socket)
|
|
28
|
+
raise File.read(log.path) if Process.waitpid(pid, Process::WNOHANG)
|
|
29
|
+
raise "display startup timeout: #{File.read(log.path)}" if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
30
|
+
sleep 0.05
|
|
31
|
+
end
|
|
32
|
+
if ARGV.include?("--ibus")
|
|
33
|
+
raise "IBus check requires dbus-run-session and X11" if wayland || !ENV["DBUS_SESSION_BUS_ADDRESS"]
|
|
34
|
+
ENV["XMODIFIERS"] = "@im=ibus"
|
|
35
|
+
ENV["LANG"] = "C.UTF-8"
|
|
36
|
+
system("ibus-daemon", "--daemonize", "--xim", "--panel=disable") or raise "IBus startup failed"
|
|
37
|
+
sleep 1
|
|
38
|
+
raise "native Japanese IME failed" unless system(RbConfig.ruby, File.join(__dir__, "native_ime.rb"))
|
|
39
|
+
ENV["XMODIFIERS"] = "@im=none"
|
|
40
|
+
end
|
|
41
|
+
success = system(RbConfig.ruby, File.join(__dir__, "native_smoke.rb"), "--gl", "--check")
|
|
42
|
+
raise "native rendering failed\n#{File.read(log.path)}" unless success
|
|
43
|
+
raise "native XDND transfer failed" unless wayland || system(RbConfig.ruby, File.join(__dir__, "native_drop.rb"))
|
|
44
|
+
raise "native watcher failed" unless system(RbConfig.ruby, File.join(__dir__, "native_watch.rb"))
|
|
45
|
+
if ARGV.include?("--editor")
|
|
46
|
+
raise "native editor integration failed" unless system(RbConfig.ruby, "--yjit", File.expand_path("../../canopus/tools/native_check.rb", __dir__), "--idle")
|
|
47
|
+
end
|
|
48
|
+
ensure
|
|
49
|
+
Process.kill("TERM", pid) rescue Errno::ESRCH
|
|
50
|
+
Process.wait(pid) rescue Errno::ECHILD
|
|
51
|
+
log.close
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
4
|
+
require "zaniah"
|
|
5
|
+
abort "Cocoa requires macOS" unless RUBY_PLATFORM.include?("darwin")
|
|
6
|
+
|
|
7
|
+
window = Zaniah::Platform.open_window(backend: :mac, width: 320, height: 200, title: "Cocoa input and resize check")
|
|
8
|
+
objc = Zaniah::FFI::ObjC
|
|
9
|
+
app = Zaniah::Platform::Mac::App.instance
|
|
10
|
+
events = []
|
|
11
|
+
window.on_input { |event| events << event }
|
|
12
|
+
window.tick
|
|
13
|
+
|
|
14
|
+
# Exercise the actual NSTextInputClient callbacks without changing the user's
|
|
15
|
+
# selected keyboard input source. Interactive IME conversion remains separate.
|
|
16
|
+
window.ime_state = Zaniah::Bounds.new(20, 30, 1, 18)
|
|
17
|
+
objc.send(window.view, "setMarkedText:selectedRange:replacementRange:", objc.string("にほん"),
|
|
18
|
+
[3, 0], [Zaniah::Platform::Mac::NS_NOT_FOUND, 0], args: [:pointer, :range, :range], result: :void)
|
|
19
|
+
raise "preedit callback missing" unless events.last.is_a?(Zaniah::Input::Composition) && events.last.text == "にほん"
|
|
20
|
+
candidate = -> { objc.send(window.view, "firstRectForCharacterRange:actualRange:", [0, 1], 0, args: [:range, :pointer], result: :rect) }
|
|
21
|
+
before = candidate.call
|
|
22
|
+
frame = objc.send(window.handle, "frame", result: :rect)
|
|
23
|
+
objc.send(window.handle, "setFrameOrigin:", [frame[0] + 25, frame[1] + 30], args: [:point], result: :void)
|
|
24
|
+
window.tick
|
|
25
|
+
moved = candidate.call
|
|
26
|
+
raise "IME candidate did not follow window move" unless (moved[0] - before[0] - 25).abs < 0.01 && (moved[1] - before[1] - 30).abs < 0.01
|
|
27
|
+
frame = objc.send(window.handle, "frame", result: :rect)
|
|
28
|
+
objc.send(window.handle, "setContentSize:", [400, 260], args: [:size], result: :void)
|
|
29
|
+
window.tick
|
|
30
|
+
raise "native resize was not delivered" unless window.content_size == Zaniah::Size.new(400, 260)
|
|
31
|
+
after_frame = objc.send(window.handle, "frame", result: :rect)
|
|
32
|
+
resized = candidate.call
|
|
33
|
+
raise "IME candidate did not follow resize" unless (resized[1] - moved[1] - (after_frame[1] - frame[1]) - 60).abs < 0.01 && resized.last(2) == [1, 18]
|
|
34
|
+
objc.send(window.view, "insertText:replacementRange:", objc.string("日本"),
|
|
35
|
+
[Zaniah::Platform::Mac::NS_NOT_FOUND, 0], args: [:pointer, :range], result: :void)
|
|
36
|
+
raise "commit callback missing" unless events.last.is_a?(Zaniah::Input::TextInput) && events.last.text == "日本"
|
|
37
|
+
|
|
38
|
+
number = objc.send(window.handle, "windowNumber", result: :long)
|
|
39
|
+
post_key = lambda do |type|
|
|
40
|
+
event = objc.send(objc.klass("NSEvent"), "keyEventWithType:location:modifierFlags:timestamp:windowNumber:context:characters:charactersIgnoringModifiers:isARepeat:keyCode:",
|
|
41
|
+
type, [0, 0], 0, Process.clock_gettime(Process::CLOCK_MONOTONIC), number, 0,
|
|
42
|
+
objc.string("a"), objc.string("a"), 0, 0,
|
|
43
|
+
args: [:ulong, :point, :ulong, :double, :long, :pointer, :pointer, :pointer, :bool, :uint])
|
|
44
|
+
objc.send(app.handle, "postEvent:atStart:", event, 0, args: [:pointer, :bool], result: :void)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# A standalone tick still drains events. The run loop also drains the entire
|
|
48
|
+
# queue before its tick callback, with no second blocking/nonblocking poll.
|
|
49
|
+
post_key.call(10)
|
|
50
|
+
window.tick
|
|
51
|
+
raise "standalone tick did not deliver key down" unless events.grep(Zaniah::Input::KeyDown).any? { |event| event.keystroke == "a" }
|
|
52
|
+
post_key.call(11)
|
|
53
|
+
post_key.call(10)
|
|
54
|
+
post_key.call(11)
|
|
55
|
+
window.on_tick do
|
|
56
|
+
raise "queued key events were delayed" unless events.grep(Zaniah::Input::KeyUp).count { |event| event.keystroke == "a" } == 2
|
|
57
|
+
raise "queued key down duplicated" unless events.grep(Zaniah::Input::KeyDown).count { |event| event.keystroke == "a" } == 2
|
|
58
|
+
window.close
|
|
59
|
+
end
|
|
60
|
+
window.run
|
|
61
|
+
puts "Cocoa: queued key down/up, standalone tick, run, Japanese composition/commit and moved/resized IME rectangles passed"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Measures native event retrieval only, not whole-application idle CPU.
|
|
4
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
5
|
+
require "zaniah"
|
|
6
|
+
require "json"
|
|
7
|
+
abort "Cocoa requires macOS" unless RUBY_PLATFORM.include?("darwin")
|
|
8
|
+
|
|
9
|
+
window = Zaniah::Platform.open_window(backend: :mac, width: 320, height: 200, title: "Cocoa event retrieval benchmark")
|
|
10
|
+
app = Zaniah::Platform::Mac::App.instance
|
|
11
|
+
100.times { app.poll }
|
|
12
|
+
samples = Array.new(15) do
|
|
13
|
+
GC.start
|
|
14
|
+
allocations = GC.stat(:total_allocated_objects)
|
|
15
|
+
cpu = Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID)
|
|
16
|
+
200.times { app.poll }
|
|
17
|
+
elapsed = Process.clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID) - cpu
|
|
18
|
+
[(GC.stat(:total_allocated_objects) - allocations) / 200.0, elapsed * 1_000_000 / 200]
|
|
19
|
+
end
|
|
20
|
+
puts JSON.generate(ruby: RUBY_DESCRIPTION, yjit: defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled?,
|
|
21
|
+
samples: samples.length, polls_per_sample: 200,
|
|
22
|
+
median_objects_per_poll: samples.map(&:first).sort[samples.length / 2],
|
|
23
|
+
median_cpu_microseconds_per_poll: samples.map(&:last).sort[samples.length / 2])
|
|
24
|
+
window.close
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
4
|
+
require "zaniah"
|
|
5
|
+
abort "This check needs macOS and a graphical login" unless RUBY_PLATFORM.include?("darwin")
|
|
6
|
+
|
|
7
|
+
window = Zaniah::Platform.open_window(backend: :mac, gpu: :metal, width: 96, height: 72, title: "Metal state reuse check")
|
|
8
|
+
begin
|
|
9
|
+
device, scene, checks = window.device, Zaniah::Scene.new, 0
|
|
10
|
+
descriptor = device.instance_variable_get(:@render_pass)
|
|
11
|
+
check = lambda do |pixels, x, y, expected|
|
|
12
|
+
scale = window.scale_factor
|
|
13
|
+
width = (device.width * scale).round
|
|
14
|
+
actual = pixels.byteslice(((y * scale).to_i * width + (x * scale).to_i) * 4, 4).unpack("C4")
|
|
15
|
+
raise "pixel (#{x}, #{y}): #{actual.inspect}, expected #{expected.inspect}" unless actual.zip(expected).all? { |a, b| (a - b).abs <= 2 }
|
|
16
|
+
checks += 1
|
|
17
|
+
end
|
|
18
|
+
render = lambda do |clear|
|
|
19
|
+
device.render(scene, clear: clear)
|
|
20
|
+
raise "render-pass descriptor was not reused" unless descriptor == device.instance_variable_get(:@render_pass)
|
|
21
|
+
device.pixels
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
clear = +"#ff000080"
|
|
25
|
+
check.call(render.call(clear), 4, 4, [255, 0, 0, 128])
|
|
26
|
+
clear.replace("#00ff0080")
|
|
27
|
+
check.call(render.call(clear), 4, 4, [0, 255, 0, 128])
|
|
28
|
+
clear = [0.0, 0.0, 1.0, 1.0]
|
|
29
|
+
check.call(render.call(clear), 4, 4, [0, 0, 255, 255])
|
|
30
|
+
clear[0], clear[2] = 1.0, 0.0
|
|
31
|
+
check.call(render.call(clear), 4, 4, [255, 0, 0, 255])
|
|
32
|
+
begin
|
|
33
|
+
render.call(nil)
|
|
34
|
+
raise "nil clear color was accepted"
|
|
35
|
+
rescue ArgumentError
|
|
36
|
+
checks += 1
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
scene.clip(Zaniah::Bounds.new(-5, -5, 15, 15)) { scene.quad(0, 0, 96, 72, color: "#f00") }
|
|
40
|
+
scene.clip(Zaniah::Bounds.new(999, 999, 10, 10)) { scene.quad(0, 0, 96, 72, color: "#0f0") }
|
|
41
|
+
scene.quad(20, 20, 12, 12, color: "#00f")
|
|
42
|
+
scene.clip(Zaniah::Bounds.new(40, 4, 20, 12)) do
|
|
43
|
+
scene.clip(Zaniah::Bounds.new(48, 0, 6, 16)) { scene.quad(0, 0, 96, 72, color: "#0f0") }
|
|
44
|
+
end
|
|
45
|
+
pixels = render.call("#000")
|
|
46
|
+
check.call(pixels, 2, 2, [255, 0, 0, 255])
|
|
47
|
+
check.call(pixels, 11, 11, [0, 0, 0, 255])
|
|
48
|
+
check.call(pixels, 24, 24, [0, 0, 255, 255])
|
|
49
|
+
check.call(pixels, 50, 8, [0, 255, 0, 255])
|
|
50
|
+
check.call(pixels, 42, 8, [0, 0, 0, 255])
|
|
51
|
+
check.call(pixels, 55, 8, [0, 0, 0, 255])
|
|
52
|
+
|
|
53
|
+
texture = device.create_texture(1, 1, data: [255, 0, 0, 255].pack("C4"))
|
|
54
|
+
scene.clear.sprite(36, 28, 8, 8, texture: texture)
|
|
55
|
+
check.call(render.call("#000"), 40, 32, [255, 0, 0, 255])
|
|
56
|
+
texture.upload(0, 0, 1, 1, [0, 255, 0, 255].pack("C4"))
|
|
57
|
+
check.call(render.call("#000"), 40, 32, [0, 255, 0, 255])
|
|
58
|
+
scene.clear.quad(8, 8, 32, 32, color: "#00f")
|
|
59
|
+
scene.quad(8, 8, 32, 32, color: "#ff000080")
|
|
60
|
+
check.call(render.call("#000"), 16, 16, [128, 0, 127, 255])
|
|
61
|
+
|
|
62
|
+
[[120, 80], [64, 48]].each do |width, height|
|
|
63
|
+
device.resize(width, height)
|
|
64
|
+
scene.clear.quad(0, 0, width, height, color: "#123")
|
|
65
|
+
pixels = render.call("#000")
|
|
66
|
+
check.call(pixels, 2, 2, [17, 34, 51, 255])
|
|
67
|
+
check.call(pixels, width - 2, height - 2, [17, 34, 51, 255])
|
|
68
|
+
end
|
|
69
|
+
puts "Metal state reuse: #{checks} checks passed, scale=#{window.scale_factor}"
|
|
70
|
+
ensure
|
|
71
|
+
window.close
|
|
72
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
4
|
+
require "zaniah"
|
|
5
|
+
|
|
6
|
+
backend = RUBY_PLATFORM.include?("darwin") ? :mac : RUBY_PLATFORM.match?(/mingw|mswin/) ? :windows : :linux
|
|
7
|
+
window = Zaniah::Platform.open_window(backend: backend, gpu: ARGV.include?("--gl") ? :opengl : backend == :mac ? :metal : :opengl, width: 800, height: 600)
|
|
8
|
+
scene = Zaniah::Scene.new
|
|
9
|
+
10_000.times { |i| scene.quad((i % 100) * 8, (i / 100) * 6, 7, 5, color: "#49c", radius: 1) }
|
|
10
|
+
window.draw { scene }
|
|
11
|
+
samples = 40.times.map do
|
|
12
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
13
|
+
window.request_frame
|
|
14
|
+
window.tick
|
|
15
|
+
(Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000
|
|
16
|
+
end.drop(5).sort
|
|
17
|
+
puts "10,000 quads: median=#{samples[samples.length / 2].round(2)}ms, p95=#{samples[(samples.length * 0.95).floor].round(2)}ms, draw_calls=#{window.device.draw_calls}"
|
|
18
|
+
window.close
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
4
|
+
require "zaniah"
|
|
5
|
+
|
|
6
|
+
backend = RUBY_PLATFORM.include?("darwin") ? :mac : :linux
|
|
7
|
+
window = Zaniah::Platform.open_window(backend: backend, width: 320, height: 180, title: "Native file drop check")
|
|
8
|
+
events = []
|
|
9
|
+
window.on_input { |event| events << event }
|
|
10
|
+
expected = "/tmp/zaniah 日本.txt"
|
|
11
|
+
if backend == :mac
|
|
12
|
+
objc = Zaniah::FFI::ObjC
|
|
13
|
+
board = objc.send(objc.klass("NSPasteboard"), "pasteboardWithUniqueName")
|
|
14
|
+
objc.send(board, "setString:forType:", objc.string("file:///tmp/zaniah%20%E6%97%A5%E6%9C%AC.txt"), objc.string("public.file-url"), args: [:pointer, :pointer], result: :bool)
|
|
15
|
+
objc.subclass("ZaniahDropFixture", "NSObject") do |klass|
|
|
16
|
+
objc.method(klass, "draggingPasteboard", result: :pointer, encoding: "@@:") { board }
|
|
17
|
+
objc.method(klass, "draggingLocation", result: :point, encoding: "{CGPoint=dd}@:") { [40.0, 40.0] }
|
|
18
|
+
end
|
|
19
|
+
dragging = objc.new("ZaniahDropFixture")
|
|
20
|
+
accepted = objc.send(window.view, "performDragOperation:", dragging, args: [:pointer], result: :bool)
|
|
21
|
+
raise "Cocoa rejected file drop" if accepted.zero?
|
|
22
|
+
window.tooltip = "Native tooltip"
|
|
23
|
+
raise "tooltip not installed" unless objc.text(objc.send(window.view, "toolTip")) == "Native tooltip"
|
|
24
|
+
changes = []
|
|
25
|
+
window.on_appearance { |value| changes << value }
|
|
26
|
+
%w[NSAppearanceNameDarkAqua NSAppearanceNameAqua].each do |name|
|
|
27
|
+
# Exported symbol names are not their string values (DarkAqua/Aqua).
|
|
28
|
+
native = objc.send(objc.klass("NSAppearance"), "appearanceNamed:", objc.string(name.delete_prefix("NSAppearanceName")), args: [:pointer])
|
|
29
|
+
objc.send(window.view, "setAppearance:", native, args: [:pointer], result: :void)
|
|
30
|
+
window.tick
|
|
31
|
+
end
|
|
32
|
+
raise "appearance notification missing: #{changes.inspect}" unless changes.include?(:dark) && changes.include?(:light)
|
|
33
|
+
objc.send(window.view, "setAppearance:", 0, args: [:pointer], result: :void)
|
|
34
|
+
objc.release(dragging)
|
|
35
|
+
objc.send(board, "releaseGlobally", result: :void)
|
|
36
|
+
else
|
|
37
|
+
raise "run this protocol check under X11" unless window.is_a?(Zaniah::Platform::Linux::Window)
|
|
38
|
+
klass = Zaniah::Platform::Linux::Window
|
|
39
|
+
ptype, long, int = klass::P, klass::L, klass::I
|
|
40
|
+
source = klass.allocate
|
|
41
|
+
source.instance_variable_set(:@x, Zaniah::FFI::Library.new("libX11.so.6"))
|
|
42
|
+
source.instance_variable_set(:@atoms, {})
|
|
43
|
+
display = source.x(:XOpenDisplay, [ptype], ptype, 0)
|
|
44
|
+
source.instance_variable_set(:@display, display)
|
|
45
|
+
root = source.x(:XDefaultRootWindow, [ptype], long, display)
|
|
46
|
+
handle = source.x(:XCreateSimpleWindow, [ptype, long, int, int, int, int, int, long, long], long, display, root, 0, 0, 40, 40, 0, 0, 0)
|
|
47
|
+
source.instance_variable_set(:@handle, handle)
|
|
48
|
+
source.x(:XSetSelectionOwner, [ptype, long, long, long], int, display, source.atom("XdndSelection"), handle, 0)
|
|
49
|
+
source.send_client_message(window.handle, "XdndEnter", [handle, 5 << 24, source.atom("text/uri-list"), 0, 0])
|
|
50
|
+
source.send_client_message(window.handle, "XdndPosition", [handle, 0, (20 << 16) | 30, 0, source.atom("XdndActionCopy")])
|
|
51
|
+
source.send_client_message(window.handle, "XdndDrop", [handle, 0, 0, 0, 0])
|
|
52
|
+
finished, deadline = false, Process.clock_gettime(Process::CLOCK_MONOTONIC) + 3
|
|
53
|
+
until finished || Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
54
|
+
window.tick
|
|
55
|
+
while source.x(:XPending, [ptype], int, display).positive?
|
|
56
|
+
event = "\0".b * 192
|
|
57
|
+
source.x(:XNextEvent, [ptype, ptype], int, display, event)
|
|
58
|
+
if event.unpack1("i") == 30
|
|
59
|
+
requestor, selection, target, property, time = event[40, 40].unpack("L!5")
|
|
60
|
+
warn "XDND request #{[requestor, selection, target, property, time].inspect}; target=#{window.handle}, source=#{handle}" if ENV["ZANIAH_DROP_DEBUG"]
|
|
61
|
+
source.property(requestor, property, target, "# file offer\r\nfile:///tmp/zaniah%20%E6%97%A5%E6%9C%AC.txt\r\n")
|
|
62
|
+
reply = "\0".b * 192
|
|
63
|
+
reply[0, 4] = [31].pack("i")
|
|
64
|
+
reply[24, 8] = [display.to_i].pack("J")
|
|
65
|
+
reply[32, 40] = [requestor, selection, target, property, time].pack("L!5")
|
|
66
|
+
source.x(:XSendEvent, [ptype, long, int, long, ptype], int, display, requestor, 0, 0, reply)
|
|
67
|
+
source.x(:XFlush, [ptype], int, display)
|
|
68
|
+
elsif event.unpack1("i") == 33 && event[40, 8].unpack1("L!") == source.atom("XdndFinished")
|
|
69
|
+
raise "XDND source did not receive success" unless event[64, 8].unpack1("L!") == 1
|
|
70
|
+
finished = true
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
sleep 0.005 unless finished
|
|
74
|
+
end
|
|
75
|
+
raise "XDND handshake timed out" unless finished
|
|
76
|
+
source.x(:XDestroyWindow, [ptype, long], int, display, handle)
|
|
77
|
+
source.x(:XCloseDisplay, [ptype], int, display)
|
|
78
|
+
end
|
|
79
|
+
drop = events.find { |event| event.is_a?(Zaniah::Input::FileDrop) }
|
|
80
|
+
raise "file drop missing or corrupted: #{events.inspect}" unless drop&.paths == [expected]
|
|
81
|
+
puts "#{window.class}: #{drop.inspect}"
|
|
82
|
+
window.close
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
4
|
+
require "zaniah"
|
|
5
|
+
require "alhena"
|
|
6
|
+
|
|
7
|
+
provider_name = ARGV.delete("--freetype") ? :freetype : :coretext
|
|
8
|
+
require "zaniah/platform/#{provider_name == :freetype ? 'linux/free_type' : 'mac/core_text'}"
|
|
9
|
+
provider = provider_name == :freetype ? Zaniah::Platform::Linux::FreeType.new : Zaniah::Platform::Mac::CoreText.new
|
|
10
|
+
font = Alhena::Font.open(ARGV.fetch(0))
|
|
11
|
+
bitmap = provider.rasterize(font, font.glyph_id("A"), size: 32)
|
|
12
|
+
raise "empty native font bitmap" unless bitmap.width.positive? && bitmap.height.positive? && bitmap.coverage.bytes.any?(&:positive?)
|
|
13
|
+
puts "#{provider_name}: #{bitmap.width}x#{bitmap.height}, bearing=#{bitmap.left},#{bitmap.top}"
|
|
14
|
+
puts bitmap.to_ascii
|
|
15
|
+
provider.close
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Run inside a disposable D-Bus/X11 session with IBus and Mozc installed.
|
|
4
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
5
|
+
require "zaniah"
|
|
6
|
+
require "open3"
|
|
7
|
+
|
|
8
|
+
window = Zaniah::Platform.open_window(backend: :linux, display_server: :x11, width: 400, height: 240, title: "IBus Japanese input check")
|
|
9
|
+
events = []
|
|
10
|
+
window.on_input { |event| events << event }
|
|
11
|
+
window.ime_state = Zaniah::Bounds.new(20, 30, 1, 20)
|
|
12
|
+
system("xdotool", "windowfocus", window.handle.to_s) or raise "focus failed"
|
|
13
|
+
ic = window.instance_variable_get(:@ic)
|
|
14
|
+
raise "XIM input context unavailable" unless ic
|
|
15
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 1
|
|
16
|
+
while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
|
|
17
|
+
window.tick
|
|
18
|
+
sleep 0.01
|
|
19
|
+
end
|
|
20
|
+
selector = Process.spawn("ibus", "engine", "mozc-on")
|
|
21
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
|
|
22
|
+
until (result = Process.waitpid2(selector, Process::WNOHANG))
|
|
23
|
+
raise "IBus engine selection timed out" if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
24
|
+
window.tick
|
|
25
|
+
sleep 0.01
|
|
26
|
+
end
|
|
27
|
+
current, = Open3.capture2e("ibus", "engine")
|
|
28
|
+
# IBus CLI may return 1 after success when Mozc's layout is "default".
|
|
29
|
+
# Its active engine, not setxkbmap's exit status, determines readiness.
|
|
30
|
+
raise "Mozc engine selection failed: #{result.last.inspect}, current=#{current.inspect}" unless current.strip == "mozc-on"
|
|
31
|
+
sender = Process.spawn("xdotool", "type", "--delay", "100", "nihon")
|
|
32
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 3
|
|
33
|
+
until Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
34
|
+
window.tick
|
|
35
|
+
sleep 0.01
|
|
36
|
+
end
|
|
37
|
+
Process.wait(sender)
|
|
38
|
+
sender = Process.spawn("xdotool", "key", "space", "Return")
|
|
39
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 2
|
|
40
|
+
until Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
41
|
+
window.tick
|
|
42
|
+
sleep 0.01
|
|
43
|
+
end
|
|
44
|
+
Process.wait(sender)
|
|
45
|
+
preedit = events.grep(Zaniah::Input::Composition).map(&:text)
|
|
46
|
+
committed = events.grep(Zaniah::Input::TextInput).map(&:text).join
|
|
47
|
+
raise "Japanese preedit missing: #{events.inspect}" unless preedit.any? { |text| text.match?(/[にほ日]/) }
|
|
48
|
+
raise "Japanese commit missing or duplicated: #{events.inspect}" unless ["日本", "にほん"].include?(committed)
|
|
49
|
+
puts "IBus/Mozc: preedit=#{preedit.inspect}, committed=#{committed.inspect}"
|
|
50
|
+
window.close
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
4
|
+
require "zaniah"
|
|
5
|
+
|
|
6
|
+
backend = RUBY_PLATFORM.include?("darwin") ? :mac : RUBY_PLATFORM.match?(/mingw|mswin/) ? :windows : :linux
|
|
7
|
+
gpu = ARGV.include?("--gl") ? :opengl : backend == :mac ? :metal : :opengl
|
|
8
|
+
window = Zaniah::Platform.open_window(backend: backend, gpu: gpu, width: 640, height: 400, title: "Zaniah UI native rendering")
|
|
9
|
+
scene = Zaniah::Scene.new
|
|
10
|
+
scene.quad(0, 0, 640, 400, color: "#18202b")
|
|
11
|
+
scene.quad(24, 24, 280, 130, color: "#46a0d8", radius: [20, 8, 20, 8], border_width: 3, border_color: "#fff")
|
|
12
|
+
scene.quad(180, 80, 260, 130, color: "#d35b7790", radius: 24)
|
|
13
|
+
texture = window.device.create_texture(2, 2, data: [255, 255, 255, 255, 80, 140, 255, 255, 80, 220, 160, 255, 0, 0, 0, 255].pack("C*"))
|
|
14
|
+
scene.sprite(28, 210, 120, 120, texture: texture)
|
|
15
|
+
scene.triangle([330, 260, 450, 340, 530, 220], color: "#f8ca72")
|
|
16
|
+
window.draw { scene }
|
|
17
|
+
window.on_input do |event|
|
|
18
|
+
puts event.inspect
|
|
19
|
+
window.close if event.is_a?(Zaniah::Input::KeyDown) && event.keystroke == "esc"
|
|
20
|
+
end
|
|
21
|
+
if ARGV.include?("--check")
|
|
22
|
+
screens = window.displays
|
|
23
|
+
raise "native display enumeration failed: #{screens.inspect}" unless screens.any? { |screen| screen.bounds.width.positive? && screen.scale_factor.positive? }
|
|
24
|
+
puts "Displays: #{screens.inspect}"
|
|
25
|
+
events = []
|
|
26
|
+
window.on_input { |event| events << event }
|
|
27
|
+
if backend == :mac
|
|
28
|
+
objc = Zaniah::FFI::ObjC
|
|
29
|
+
objc.send(window.view, "setMarkedText:selectedRange:replacementRange:", objc.string("にほん"), [3, 0], [(1 << 63) - 1, 0], args: [:pointer, :range, :range], result: :void)
|
|
30
|
+
raise "marked text callback failed" unless events.last.is_a?(Zaniah::Input::Composition) && events.last.text == "にほん"
|
|
31
|
+
objc.send(window.view, "insertText:replacementRange:", objc.string("日本"), [(1 << 63) - 1, 0], args: [:pointer, :range], result: :void)
|
|
32
|
+
raise "committed text callback failed" unless events.last.is_a?(Zaniah::Input::TextInput) && events.last.text == "日本"
|
|
33
|
+
window.ime_state = Zaniah::Bounds.new(20, 30, 1, 18)
|
|
34
|
+
candidate = objc.send(window.view, "firstRectForCharacterRange:actualRange:", [0, 1], 0, args: [:range, :pointer], result: :rect)
|
|
35
|
+
raise "IME candidate rectangle failed" unless candidate.last(2) == [1, 18]
|
|
36
|
+
elsif backend == :linux && window.is_a?(Zaniah::Platform::Linux::Window) && ENV["ZANIAH_CHECK_INPUT"]
|
|
37
|
+
system("xdotool", "windowfocus", window.handle.to_s, "key", "a", "mousemove", "--window", window.handle.to_s, "40", "40", "click", "1", "click", "5") or raise "xdotool input failed"
|
|
38
|
+
3.times { window.tick }
|
|
39
|
+
raise "X11 keyboard text missing: #{events.inspect}" unless events.any? { |event| event.is_a?(Zaniah::Input::TextInput) && event.text == "a" }
|
|
40
|
+
raise "X11 mouse button missing" unless events.any? { |event| event.is_a?(Zaniah::Input::MouseDown) }
|
|
41
|
+
raise "X11 scroll-down direction reversed" unless events.any? { |event| event.is_a?(Zaniah::Input::ScrollWheel) && event.delta.y.positive? }
|
|
42
|
+
end
|
|
43
|
+
3.times { window.request_frame; window.tick }
|
|
44
|
+
output = ARGV.find { |argument| argument.end_with?(".png") }
|
|
45
|
+
window.write_png(output) if output
|
|
46
|
+
scale = window.scale_factor
|
|
47
|
+
pixels = window.device.pixels
|
|
48
|
+
pixel_width = (window.content_size.width * scale).round
|
|
49
|
+
actual = pixels.byteslice(((40 * scale).to_i * pixel_width + (40 * scale).to_i) * 4, 4).unpack("C4")
|
|
50
|
+
expected = [70, 160, 216, 255]
|
|
51
|
+
raise "GPU pixel mismatch: #{actual.inspect}" unless actual.zip(expected).all? { |a, b| (a - b).abs <= 2 }
|
|
52
|
+
puts "#{window.class}/#{gpu}: pixel=#{actual.inspect}, draw_calls=#{window.device.draw_calls}, scale=#{scale}"
|
|
53
|
+
if backend == :linux && window.is_a?(Zaniah::Platform::Linux::Window) && ENV["ZANIAH_CHECK_DPI"]
|
|
54
|
+
# linux_smoke owns this disposable X server. Restore its resource property.
|
|
55
|
+
root, property = window.instance_variable_get(:@root), window.atom("RESOURCE_MANAGER")
|
|
56
|
+
previous = window.read_property(root, property, delete: false)
|
|
57
|
+
begin
|
|
58
|
+
changed = previous.gsub(/^Xft\.dpi:.*$/, "") + "\nXft.dpi: 192\n"
|
|
59
|
+
window.property(root, property, window.atom("STRING"), changed)
|
|
60
|
+
window.x(:XFlush, [Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT, window.display)
|
|
61
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 1
|
|
62
|
+
until window.scale_factor == 2
|
|
63
|
+
raise "X11 DPI change was not delivered" if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
64
|
+
window.tick
|
|
65
|
+
end
|
|
66
|
+
puts "X11 live DPI: #{scale} -> #{window.scale_factor}"
|
|
67
|
+
ensure
|
|
68
|
+
window.property(root, property, window.atom("STRING"), previous)
|
|
69
|
+
window.x(:XFlush, [Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT, window.display)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
window.close
|
|
73
|
+
else
|
|
74
|
+
window.run
|
|
75
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
4
|
+
require "zaniah"
|
|
5
|
+
require "zaniah/gpu/instance_packing"
|
|
6
|
+
|
|
7
|
+
backend = RUBY_PLATFORM.include?("darwin") ? :mac : RUBY_PLATFORM.match?(/mingw|mswin/) ? :windows : :linux
|
|
8
|
+
gpu = ARGV.include?("--gl") ? :opengl : backend == :mac ? :metal : :opengl
|
|
9
|
+
font_path = File.expand_path("../assets/fonts/Abel-Regular.ttf", __dir__)
|
|
10
|
+
database = Zaniah::TextSystem::FontDB.new(paths: [font_path])
|
|
11
|
+
system = Zaniah::TextSystem::Renderer.new(font_db: database)
|
|
12
|
+
window = Zaniah::Platform.open_window(backend: backend, gpu: gpu, width: 800, height: 640, title: "Cached 10,000-glyph text check")
|
|
13
|
+
system.scale_factor = window.scale_factor
|
|
14
|
+
texts = Array.new(100) { |i| (format("%03d", i) + "Abcdefghij" * 10).byteslice(0, 100).freeze }
|
|
15
|
+
scene = Zaniah::Scene.new
|
|
16
|
+
window.draw do
|
|
17
|
+
scene.clear
|
|
18
|
+
texts.each_with_index do |text, i|
|
|
19
|
+
system.paint_line(scene, system.layout_line(text, size: 7), x: 4, y: 8 + i * 6, color: "#fff")
|
|
20
|
+
end
|
|
21
|
+
scene
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
samples, allocations = [], []
|
|
25
|
+
40.times do |i|
|
|
26
|
+
before = GC.stat(:total_allocated_objects)
|
|
27
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
28
|
+
window.request_frame
|
|
29
|
+
window.tick
|
|
30
|
+
elapsed = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000
|
|
31
|
+
objects = GC.stat(:total_allocated_objects) - before
|
|
32
|
+
if i >= 5
|
|
33
|
+
samples << elapsed
|
|
34
|
+
allocations << objects
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
bytes, batches = Zaniah::GPU::InstancePacking.pack(scene)
|
|
38
|
+
raise "expected 10,000 packed glyphs" unless bytes.bytesize == 10_000 * 96
|
|
39
|
+
pixels = window.device.pixels
|
|
40
|
+
ink, offset = 0, 0
|
|
41
|
+
while offset < pixels.bytesize
|
|
42
|
+
ink += 1 if pixels.getbyte(offset) > 20
|
|
43
|
+
offset += 4
|
|
44
|
+
end
|
|
45
|
+
raise "native packed text readback was empty" unless ink > 1000
|
|
46
|
+
output = ARGV.find { |argument| argument.end_with?(".png") }
|
|
47
|
+
window.write_png(output) if output
|
|
48
|
+
calls = 0
|
|
49
|
+
trace = TracePoint.new(:c_call) { |event| calls += 1 if event.method_id == :call && event.defined_class == Fiddle::Function }
|
|
50
|
+
trace.enable { window.request_frame; window.tick }
|
|
51
|
+
puts "#{gpu}: 10,000 glyphs, present median=#{samples.sort[samples.length / 2].round(3)}ms p95=#{samples.sort[(samples.length * 0.95).floor].round(3)}ms allocations=#{allocations.sort[allocations.length / 2]} Fiddle calls=#{calls}"
|
|
52
|
+
puts "readback=#{ink} ink pixels, #{bytes.bytesize} instance bytes, batches=#{batches.length}, draw_calls=#{window.device.draw_calls}, scale=#{window.scale_factor}"
|
|
53
|
+
require "digest"
|
|
54
|
+
puts "rgba_sha256=#{Digest::SHA256.hexdigest(pixels)}"
|
|
55
|
+
if ARGV.include?("--allocations")
|
|
56
|
+
require "objspace"
|
|
57
|
+
GC.start
|
|
58
|
+
generation = GC.count
|
|
59
|
+
previously_disabled = GC.disable
|
|
60
|
+
begin
|
|
61
|
+
ObjectSpace.trace_object_allocations_start
|
|
62
|
+
window.request_frame
|
|
63
|
+
window.tick
|
|
64
|
+
ObjectSpace.trace_object_allocations_stop
|
|
65
|
+
sites = Hash.new(0)
|
|
66
|
+
ObjectSpace.each_object do |object|
|
|
67
|
+
file = ObjectSpace.allocation_sourcefile(object)
|
|
68
|
+
next unless file && ObjectSpace.allocation_generation(object) == generation
|
|
69
|
+
sites[[file, ObjectSpace.allocation_sourceline(object), object.class]] += 1
|
|
70
|
+
end
|
|
71
|
+
sites.sort_by { |_, count| -count }.first(25).each { |(file, line, type), count| puts "#{count} #{type} #{file}:#{line}" }
|
|
72
|
+
ensure
|
|
73
|
+
ObjectSpace.trace_object_allocations_stop
|
|
74
|
+
ObjectSpace.trace_object_allocations_clear
|
|
75
|
+
GC.enable unless previously_disabled
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
system.close
|
|
79
|
+
window.close
|
|
80
|
+
if ARGV.include?("--budget")
|
|
81
|
+
raise "native cached text frame exceeds 8ms median" if samples.sort[samples.length / 2] > 8
|
|
82
|
+
raise "native cached text frame exceeds 200 objects" if allocations.sort[allocations.length / 2] >= 200
|
|
83
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
4
|
+
require "zaniah"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
|
|
7
|
+
platform = RUBY_PLATFORM.include?("darwin") ? :mac : RUBY_PLATFORM.match?(/mingw|mswin/) ? :windows : :linux
|
|
8
|
+
require "zaniah/platform/#{platform}/watcher"
|
|
9
|
+
Dir.mktmpdir("zaniah-watch-") do |directory|
|
|
10
|
+
watcher = Zaniah::Platform.const_get(platform.to_s.capitalize)::Watcher.new(directory)
|
|
11
|
+
begin
|
|
12
|
+
path = File.join(directory, "native-event.txt")
|
|
13
|
+
File.write(path, "filesystem notification")
|
|
14
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
|
|
15
|
+
events = []
|
|
16
|
+
loop do
|
|
17
|
+
events.concat(watcher.poll(timeout: 0.05))
|
|
18
|
+
break if events.any? { |event| event.path&.end_with?("native-event.txt") }
|
|
19
|
+
raise "native watcher received no file event: #{events.inspect}" if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
20
|
+
end
|
|
21
|
+
puts events.inspect
|
|
22
|
+
ensure
|
|
23
|
+
watcher.close
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
4
|
+
$LOAD_PATH.unshift(File.expand_path("../../electra/lib", __dir__))
|
|
5
|
+
require "zaniah"
|
|
6
|
+
require "zaniah/gpu/vulkan"
|
|
7
|
+
require_relative "../../electra/examples/shaders"
|
|
8
|
+
|
|
9
|
+
device = Zaniah::GPU::Vulkan.new(width: 128, height: 128)
|
|
10
|
+
begin
|
|
11
|
+
pixels = device.render_triangle(vertex_spirv: ExampleShaders.triangle_vertex, fragment_spirv: ExampleShaders.triangle_fragment)
|
|
12
|
+
center = pixels.byteslice((64 * 128 + 64) * 4, 4).unpack("C4")
|
|
13
|
+
outside = pixels.byteslice(0, 4).unpack("C4")
|
|
14
|
+
raise "Vulkan triangle pixel mismatch: #{center.inspect}" unless center.zip([255, 64, 0, 255]).all? { |actual, expected| (actual - expected).abs <= 1 }
|
|
15
|
+
raise "Vulkan clear pixel mismatch: #{outside.inspect}" unless outside == [0, 0, 0, 255]
|
|
16
|
+
device.write_png(ARGV.first) if ARGV.first
|
|
17
|
+
puts "Vulkan #{device.device_name}: triangle=#{center.inspect}, clear=#{outside.inspect}"
|
|
18
|
+
ensure
|
|
19
|
+
device.release
|
|
20
|
+
end
|