初始提交:terminalX 可运行态(M0/M1/M1.5 已真机验证)
- M0: libghostty SSH 终端(渲染/输入/连接)+ 白屏修复(OutputGate) + 会话状态机/自动重连 - M1: tsnet 用户态组网 + SSH-over-tsnet(fd 桥),shell 级真机验证;R5(Go+gvisor+C+++Swift 同进程) retire - M1.5: tmux -CC 原生 tab(MVP) - 结构: packages/(TXCore·TXTransport), apps/TerminalX, vendor/(libghostty-spm/libssh2/mbedtls/tsnet-bridge), artifacts/ - 文档: CLAUDE.md + docs/HANDOFF.md(新会话入口) - 环境: 认证代理→依赖 vendor 本地化;Go 在 ~/.local/go;仅模拟器/未签名 - 待续: M2 mosh, tmux 多 pane, M4 安全(host key/SE) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
473
vendor/libghostty-spm/Patches/ghostty/0005-ios-metal-rendering.sh
vendored
Executable file
473
vendor/libghostty-spm/Patches/ghostty/0005-ios-metal-rendering.sh
vendored
Executable file
@@ -0,0 +1,473 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SOURCE_DIR="${1:?Usage: $0 <ghostty-source-dir>}"
|
||||
|
||||
# =============================================================================
|
||||
# Patch 1: IOSurfaceLayer — iOS rendering compatibility
|
||||
#
|
||||
# Problem: On iOS, IOSurface dimensions may differ by ±1 pixel from the
|
||||
# CALayer bounds due to rounding differences between UIKit point-to-pixel
|
||||
# conversion and Metal's integer pixel sizes. The upstream code does an
|
||||
# exact match and silently discards the surface, causing a blank screen.
|
||||
#
|
||||
# Additionally, iOS doesn't have a native CALayer subclass optimized for
|
||||
# IOSurface content display. Using the private CAIOSurfaceLayer class
|
||||
# (available since iOS 11) provides hardware-accelerated compositing.
|
||||
#
|
||||
# Fix:
|
||||
# - Allow ±1px tolerance on iOS when comparing surface vs layer dimensions
|
||||
# - Dynamically adjust contentsScale when dimensions don't match exactly
|
||||
# - Use CAIOSurfaceLayer as base class on iOS for native IOSurface compositing
|
||||
# - Mark layer as opaque since terminal content fills the entire bounds
|
||||
# =============================================================================
|
||||
IOSURFACE_LAYER="${SOURCE_DIR}/src/renderer/metal/IOSurfaceLayer.zig"
|
||||
if [ -f "$IOSURFACE_LAYER" ]; then
|
||||
if grep -q 'const log = std.log.scoped(.IOSurfaceLayer);' "$IOSURFACE_LAYER"; then
|
||||
python3 - "$IOSURFACE_LAYER" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
src = path.read_text()
|
||||
|
||||
# Need builtin for comptime os.tag checks
|
||||
src = src.replace(
|
||||
'const std = @import("std");\nconst Allocator = std.mem.Allocator;',
|
||||
'const std = @import("std");\nconst builtin = @import("builtin");\nconst Allocator = std.mem.Allocator;'
|
||||
)
|
||||
|
||||
# The scoped log is only used in the size check we're replacing; drop it
|
||||
src = src.replace('\nconst log = std.log.scoped(.IOSurfaceLayer);\n', '\n')
|
||||
|
||||
# Terminal surface is always fully opaque — tell the compositor
|
||||
src = src.replace(
|
||||
'layer.setProperty("contentsGravity", macos.animation.kCAGravityTopLeft);\n\n layer.setInstanceVariable',
|
||||
'layer.setProperty("contentsGravity", macos.animation.kCAGravityTopLeft);\n layer.setProperty("opaque", true);\n\n layer.setInstanceVariable'
|
||||
)
|
||||
|
||||
# Replace the strict size equality check with a platform-aware version.
|
||||
# On iOS, UIKit's point→pixel rounding can produce a 1px discrepancy.
|
||||
# Rather than dropping the frame entirely (→ blank screen), we accept it
|
||||
# and recalculate contentsScale so CoreAnimation stretches correctly.
|
||||
old_block = """ if (width != surface.getWidth() or height != surface.getHeight()) {
|
||||
log.debug(
|
||||
"setSurfaceCallback(): surface is wrong size for layer, discarding. surface = {d}x{d}, layer = {d}x{d}",
|
||||
.{ surface.getWidth(), surface.getHeight(), width, height },
|
||||
);
|
||||
return;
|
||||
}"""
|
||||
|
||||
new_block = """ const sw = surface.getWidth();
|
||||
const sh = surface.getHeight();
|
||||
const dw: usize = if (width > sw) width - sw else sw - width;
|
||||
const dh: usize = if (height > sh) height - sh else sh - height;
|
||||
// iOS UIKit rounding can produce ±1px discrepancy; macOS must match exactly
|
||||
const max_drift: usize = if (comptime builtin.os.tag == .ios) 1 else 0;
|
||||
if (dw > max_drift or dh > max_drift) {
|
||||
if (comptime builtin.os.tag == .ios) {
|
||||
// Recalculate contentsScale so CA maps surface pixels to layer points
|
||||
const pw = bounds.size.width;
|
||||
const ph = bounds.size.height;
|
||||
if (pw > 0 and ph > 0) {
|
||||
const cs_x: f64 = @as(f64, @floatFromInt(sw)) / pw;
|
||||
const cs_y: f64 = @as(f64, @floatFromInt(sh)) / ph;
|
||||
const cs: f64 = @max(cs_x, cs_y);
|
||||
if (@abs(cs - scale) > 0.01) {
|
||||
layer.setProperty("contentsScale", cs);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}"""
|
||||
|
||||
if old_block not in src:
|
||||
print("[!] IOSurfaceLayer size check block not found — source may have changed")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_block, new_block)
|
||||
|
||||
# Use the system-provided CAIOSurfaceLayer on iOS; it handles
|
||||
# IOSurface display natively with zero-copy compositing.
|
||||
old_cls = """ const CALayer =
|
||||
objc.getClass("CALayer") orelse return error.ObjCFailed;
|
||||
|
||||
var subclass =
|
||||
objc.allocateClassPair(CALayer, "IOSurfaceLayer") orelse return error.ObjCFailed;"""
|
||||
|
||||
new_cls = """ const parent_cls = if (comptime builtin.os.tag == .ios)
|
||||
// CAIOSurfaceLayer provides native zero-copy IOSurface compositing
|
||||
objc.getClass("CAIOSurfaceLayer") orelse
|
||||
objc.getClass("CALayer") orelse return error.ObjCFailed
|
||||
else
|
||||
objc.getClass("CALayer") orelse return error.ObjCFailed;
|
||||
|
||||
var subclass =
|
||||
objc.allocateClassPair(parent_cls, "IOSurfaceLayer") orelse return error.ObjCFailed;"""
|
||||
|
||||
src = src.replace(old_cls, new_cls)
|
||||
|
||||
path.write_text(src)
|
||||
print("[+] patched IOSurfaceLayer: iOS size tolerance + CAIOSurfaceLayer")
|
||||
PY
|
||||
else
|
||||
echo "[+] IOSurfaceLayer already patched"
|
||||
fi
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Patch 2: Metal.zig — iOS first-frame display + synchronous present
|
||||
#
|
||||
# Problem 1: On iOS the IOSurfaceLayer is added as a sublayer of the UIView's
|
||||
# backing layer. By the time the renderer registers its display callback the
|
||||
# sublayer already has its bounds set, so no "display" message is generated.
|
||||
# The first frame never renders.
|
||||
#
|
||||
# Problem 2: The async present path dispatches to the main thread via GCD.
|
||||
# On iOS the render loop already runs on the main thread, so the async
|
||||
# dispatch adds an unnecessary runloop turn of latency and can cause ordering
|
||||
# issues with UIKit layout.
|
||||
#
|
||||
# Fix:
|
||||
# - Call setNeedsDisplay after registering the display callback on iOS
|
||||
# - On iOS, always use the synchronous present path (setSurface checks
|
||||
# isMainThread internally and runs inline when true)
|
||||
# =============================================================================
|
||||
METAL_ZIG="${SOURCE_DIR}/src/renderer/Metal.zig"
|
||||
if [ -f "$METAL_ZIG" ]; then
|
||||
if ! grep -q 'setNeedsDisplay' "$METAL_ZIG"; then
|
||||
python3 - "$METAL_ZIG" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
src = path.read_text()
|
||||
|
||||
# Kick the first display cycle after callback registration on iOS
|
||||
old_cb = """ @ptrCast(&displayCallback),
|
||||
@ptrCast(renderer),
|
||||
);
|
||||
}"""
|
||||
|
||||
new_cb = """ @ptrCast(&displayCallback),
|
||||
@ptrCast(renderer),
|
||||
);
|
||||
|
||||
// iOS: sublayer bounds are already set before the callback is wired up,
|
||||
// so no display message fires automatically. Kick the first frame.
|
||||
if (comptime builtin.os.tag == .ios) {
|
||||
self.layer.layer.msgSend(void, objc.sel("setNeedsDisplay"), .{});
|
||||
}
|
||||
}"""
|
||||
|
||||
if old_cb not in src:
|
||||
print("[!] Metal loopEnter callback not found")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_cb, new_cb)
|
||||
|
||||
# iOS render loop is main-thread; skip the async dispatch path entirely.
|
||||
old_present = """pub inline fn present(self: *Metal, target: Target, sync: bool) !void {
|
||||
if (sync) {
|
||||
self.layer.setSurfaceSync(target.surface);
|
||||
} else {
|
||||
try self.layer.setSurface(target.surface);
|
||||
}
|
||||
}"""
|
||||
|
||||
new_present = """pub inline fn present(self: *Metal, target: Target, sync: bool) !void {
|
||||
// iOS: always present synchronously — the render loop already runs on
|
||||
// the main thread, so the async GCD hop is unnecessary overhead.
|
||||
if (comptime builtin.os.tag == .ios) {
|
||||
try self.layer.setSurface(target.surface);
|
||||
return;
|
||||
}
|
||||
if (sync) {
|
||||
self.layer.setSurfaceSync(target.surface);
|
||||
} else {
|
||||
try self.layer.setSurface(target.surface);
|
||||
}
|
||||
}"""
|
||||
|
||||
if old_present not in src:
|
||||
print("[!] Metal present function not found")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_present, new_present)
|
||||
|
||||
path.write_text(src)
|
||||
print("[+] patched Metal.zig: iOS first-frame trigger + sync present")
|
||||
PY
|
||||
else
|
||||
echo "[+] Metal.zig already patched"
|
||||
fi
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Patch 3: coretext.zig — Skip CF release thread on iOS
|
||||
#
|
||||
# Problem: The CoreText font shaper spawns a background thread that uses
|
||||
# libxev's kqueue event loop to asynchronously release CoreFoundation
|
||||
# objects. On iOS, kqueue's Mach port allocation fails (sandbox restrictions
|
||||
# + simulator incompatibility), crashing the thread and potentially stalling
|
||||
# font shaping operations.
|
||||
#
|
||||
# Fix: Make the CF release thread optional. On iOS, skip thread creation
|
||||
# entirely and release CF objects synchronously in endFrame(). This is
|
||||
# acceptable because iOS devices have fast enough CF release performance
|
||||
# and the terminal doesn't produce the same volume of shaped text as a
|
||||
# desktop compositor.
|
||||
# =============================================================================
|
||||
CORETEXT="${SOURCE_DIR}/src/font/shaper/coretext.zig"
|
||||
if [ -f "$CORETEXT" ]; then
|
||||
if grep -q 'cf_release_thread: \*CFReleaseThread,' "$CORETEXT"; then
|
||||
python3 - "$CORETEXT" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
src = path.read_text()
|
||||
|
||||
# Make the struct fields optional so nil can represent "no thread"
|
||||
src = src.replace(
|
||||
'cf_release_thread: *CFReleaseThread,\n cf_release_thr: std.Thread,',
|
||||
'cf_release_thread: ?*CFReleaseThread,\n cf_release_thr: ?std.Thread,'
|
||||
)
|
||||
|
||||
# Guard thread creation behind a comptime platform check
|
||||
old_create = """ // Create the CF release thread.
|
||||
var cf_release_thread = try alloc.create(CFReleaseThread);
|
||||
errdefer alloc.destroy(cf_release_thread);
|
||||
cf_release_thread.* = try .init(alloc);
|
||||
errdefer cf_release_thread.deinit();
|
||||
|
||||
// Start the CF release thread.
|
||||
var cf_release_thr = try std.Thread.spawn(
|
||||
.{},
|
||||
CFReleaseThread.threadMain,
|
||||
.{cf_release_thread},
|
||||
);
|
||||
cf_release_thr.setName("cf_release") catch {};
|
||||
|
||||
return .{"""
|
||||
|
||||
new_create = """ // On iOS the kqueue-based event loop used by the release thread
|
||||
// crashes due to Mach port sandbox restrictions. Skip it entirely
|
||||
// and fall through to synchronous release in endFrame.
|
||||
var cf_release_thread: ?*CFReleaseThread = null;
|
||||
var cf_release_thr: ?std.Thread = null;
|
||||
if (comptime builtin.os.tag != .ios) {
|
||||
const thr_obj = try alloc.create(CFReleaseThread);
|
||||
errdefer alloc.destroy(thr_obj);
|
||||
thr_obj.* = try .init(alloc);
|
||||
errdefer thr_obj.deinit();
|
||||
const thr = try std.Thread.spawn(.{}, CFReleaseThread.threadMain, .{thr_obj});
|
||||
thr.setName("cf_release") catch {};
|
||||
cf_release_thread = thr_obj;
|
||||
cf_release_thr = thr;
|
||||
}
|
||||
|
||||
return .{"""
|
||||
|
||||
if old_create not in src:
|
||||
print("[!] coretext CF release thread creation block not found")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_create, new_create)
|
||||
|
||||
# Deinit: only join/stop the thread if it was created
|
||||
old_deinit = """ // Stop the CF release thread
|
||||
{
|
||||
self.cf_release_thread.stop.notify() catch |err|
|
||||
log.err("error notifying cf release thread to stop, may stall err={}", .{err});
|
||||
self.cf_release_thr.join();
|
||||
}
|
||||
self.cf_release_thread.deinit();
|
||||
self.alloc.destroy(self.cf_release_thread);"""
|
||||
|
||||
new_deinit = """ // Stop the CF release thread (nil on iOS)
|
||||
if (self.cf_release_thread) |thr_obj| {
|
||||
thr_obj.stop.notify() catch |err|
|
||||
log.err("error notifying cf release thread to stop, may stall err={}", .{err});
|
||||
self.cf_release_thr.?.join();
|
||||
thr_obj.deinit();
|
||||
self.alloc.destroy(thr_obj);
|
||||
}"""
|
||||
|
||||
if old_deinit not in src:
|
||||
print("[!] coretext CF release thread deinit block not found")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_deinit, new_deinit)
|
||||
|
||||
# endFrame: guard the mailbox push behind an optional check.
|
||||
# When nil (iOS), fall through to the synchronous release below.
|
||||
old_end = """ // Send the items. If the send succeeds then we wake up the
|
||||
// thread to process the items. If the send fails then do a manual
|
||||
// cleanup.
|
||||
if (self.cf_release_thread.mailbox.push(.{ .release = .{
|
||||
.refs = items,
|
||||
.alloc = self.alloc,
|
||||
} }, .{ .forever = {} }) != 0) {
|
||||
self.cf_release_thread.wakeup.notify() catch |err| {
|
||||
log.warn(
|
||||
"error notifying cf release thread to wake up, may stall err={}",
|
||||
.{err},
|
||||
);
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
for (items) |ref| macos.foundation.CFRelease(ref);"""
|
||||
|
||||
new_end = """ // Offload to the background release thread when available.
|
||||
// On iOS cf_release_thread is nil, so we fall through to sync release.
|
||||
if (self.cf_release_thread) |thr_obj| {
|
||||
if (thr_obj.mailbox.push(.{ .release = .{
|
||||
.refs = items,
|
||||
.alloc = self.alloc,
|
||||
} }, .{ .forever = {} }) != 0) {
|
||||
thr_obj.wakeup.notify() catch |err| {
|
||||
log.warn(
|
||||
"error notifying cf release thread to wake up, may stall err={}",
|
||||
.{err},
|
||||
);
|
||||
};
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (items) |ref| macos.foundation.CFRelease(ref);"""
|
||||
|
||||
if old_end not in src:
|
||||
print("[!] coretext endFrame mailbox block not found")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_end, new_end)
|
||||
|
||||
path.write_text(src)
|
||||
print("[+] patched coretext.zig: CF release thread disabled on iOS")
|
||||
PY
|
||||
else
|
||||
echo "[+] coretext.zig already patched"
|
||||
fi
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Patch 4: iosurface.zig — Explicit row byte alignment
|
||||
#
|
||||
# Problem: When creating an IOSurface without specifying bytesPerRow, the
|
||||
# system picks whatever alignment it wants. Metal textures created from
|
||||
# these surfaces may have mismatched row strides, causing corrupted or
|
||||
# shifted glyph rendering (especially visible on font atlas textures).
|
||||
#
|
||||
# Fix: Calculate 64-byte-aligned row bytes and pass kIOSurfaceBytesPerRow
|
||||
# when creating the IOSurface. Also suppress unused return value warnings
|
||||
# from IOSurfaceLock/Unlock.
|
||||
# =============================================================================
|
||||
IOSURFACE="${SOURCE_DIR}/pkg/macos/iosurface/iosurface.zig"
|
||||
if [ -f "$IOSURFACE" ]; then
|
||||
if ! grep -q 'kIOSurfaceBytesPerRow' "$IOSURFACE"; then
|
||||
python3 - "$IOSURFACE" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
src = path.read_text()
|
||||
|
||||
# Compute aligned row stride before creating the Number objects
|
||||
old_start = """ pub fn init(properties: Properties) Allocator.Error!*IOSurface {
|
||||
var w = try foundation.Number.create(.int, &properties.width);"""
|
||||
|
||||
new_start = """ pub fn init(properties: Properties) Allocator.Error!*IOSurface {
|
||||
// Ensure row stride is 64-byte aligned for Metal texture compatibility.
|
||||
const aligned_stride: c_int = @intCast(
|
||||
(properties.width * properties.bytes_per_element + 63) & ~@as(c_int, 63),
|
||||
);
|
||||
var w = try foundation.Number.create(.int, &properties.width);"""
|
||||
|
||||
if old_start not in src:
|
||||
print("[!] iosurface init start not found")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_start, new_start)
|
||||
|
||||
# Create a Number for the stride and include it in the dictionary
|
||||
old_dict_setup = """ var bpe = try foundation.Number.create(.int, &properties.bytes_per_element);
|
||||
defer bpe.release();
|
||||
|
||||
var properties_dict = try foundation.Dictionary.create("""
|
||||
|
||||
new_dict_setup = """ var bpe = try foundation.Number.create(.int, &properties.bytes_per_element);
|
||||
defer bpe.release();
|
||||
|
||||
var stride_num = try foundation.Number.create(.int, &aligned_stride);
|
||||
defer stride_num.release();
|
||||
|
||||
var properties_dict = try foundation.Dictionary.create("""
|
||||
|
||||
if old_dict_setup not in src:
|
||||
print("[!] iosurface bpe block not found")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_dict_setup, new_dict_setup)
|
||||
|
||||
# Extend the dictionary keys/values arrays
|
||||
old_dict = """ &[_]?*const anyopaque{
|
||||
c.kIOSurfaceWidth,
|
||||
c.kIOSurfaceHeight,
|
||||
c.kIOSurfacePixelFormat,
|
||||
c.kIOSurfaceBytesPerElement,
|
||||
},
|
||||
&[_]?*const anyopaque{ w, h, pf, bpe },"""
|
||||
|
||||
new_dict = """ &[_]?*const anyopaque{
|
||||
c.kIOSurfaceWidth,
|
||||
c.kIOSurfaceHeight,
|
||||
c.kIOSurfacePixelFormat,
|
||||
c.kIOSurfaceBytesPerElement,
|
||||
c.kIOSurfaceBytesPerRow,
|
||||
},
|
||||
&[_]?*const anyopaque{ w, h, pf, bpe, stride_num },"""
|
||||
|
||||
if old_dict not in src:
|
||||
print("[!] iosurface dictionary keys not found")
|
||||
sys.exit(1)
|
||||
src = src.replace(old_dict, new_dict)
|
||||
|
||||
# Silence unused return value from IOSurfaceLock/Unlock
|
||||
src = src.replace(
|
||||
' c.IOSurfaceLock(\n @ptrCast(self),\n 0,\n null,\n );',
|
||||
' _ = c.IOSurfaceLock(\n @ptrCast(self),\n 0,\n null,\n );'
|
||||
)
|
||||
src = src.replace(
|
||||
' c.IOSurfaceUnlock(\n @ptrCast(self),\n 0,\n null,\n );',
|
||||
' _ = c.IOSurfaceUnlock(\n @ptrCast(self),\n 0,\n null,\n );'
|
||||
)
|
||||
|
||||
path.write_text(src)
|
||||
print("[+] patched iosurface.zig: 64-byte stride alignment for Metal")
|
||||
PY
|
||||
else
|
||||
echo "[+] iosurface.zig already patched"
|
||||
fi
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# Patch 5: build.zig.zon — Update libxev to fix iOS kqueue mach port panic
|
||||
#
|
||||
# Problem: The bundled libxev uses mach ports for async wakeup on Darwin.
|
||||
# Its kqueue backend checks `os.tag != .macos` and returns null for mach port
|
||||
# kevents on non-macOS Darwin (iOS). The caller then unwraps null with `.?`
|
||||
# causing a panic. A newer libxev version fixes this by properly supporting
|
||||
# iOS as a Darwin target.
|
||||
#
|
||||
# Fix: Update the libxev dependency URL and hash to a version that handles
|
||||
# iOS mach ports correctly.
|
||||
# =============================================================================
|
||||
BUILD_ZON="${SOURCE_DIR}/build.zig.zon"
|
||||
if [ -f "$BUILD_ZON" ]; then
|
||||
if ! grep -q '7e7d2f2ab4700544657f8ec268715c8ef320d839' "$BUILD_ZON"; then
|
||||
sed -i '' 's|"https://deps.files.ghostty.org/libxev-34fa50878aec6e5fa8f532867001ab3c36fae23e.tar.gz"|"https://github.com/mitchellh/libxev/archive/7e7d2f2ab4700544657f8ec268715c8ef320d839.tar.gz"|' "$BUILD_ZON"
|
||||
sed -i '' 's|"libxev-0.0.0-86vtc4IcEwCqEYxEYoN_3KXmc6A9VLcm22aVImfvecYs"|"libxev-0.0.0-86vtcwE9EwB942iWRnaNMXHv3n0BeLAs_tVhrs5cT8cQ"|' "$BUILD_ZON"
|
||||
echo "[+] patched build.zig.zon: updated libxev for iOS mach port fix"
|
||||
else
|
||||
echo "[+] libxev already updated"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "[+] all ios metal rendering patches applied"
|
||||
Reference in New Issue
Block a user