Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -238,12 +238,18 @@ jobs:
echo "Using $WINDEPLOY"
"$WINDEPLOY" --release --no-translations --dir "$STAGE" --compiler-runtime "$STAGE/ryty-gui.exe"

# Defensive: make sure the MinGW runtime DLLs are present even if
# windeployqt skipped them.
for dll in libgcc_s_seh-1.dll libstdc++-6.dll libwinpthread-1.dll zlib1.dll; do
if [ ! -f "$STAGE/$dll" ] && [ -f "/ucrt64/bin/$dll" ]; then
cp "/ucrt64/bin/$dll" "$STAGE/"
fi
# Defensive: make sure all dependent MinGW and UCRT64 runtime DLLs are present
# even if windeployqt skipped them.
for iter in 1 2 3; do
for file in "$STAGE"/*.exe "$STAGE"/*.dll "$STAGE"/platforms/*.dll "$STAGE"/styles/*.dll "$STAGE"/imageformats/*.dll; do
if [ -f "$file" ]; then
(ldd "$file" 2>/dev/null | grep -i "/ucrt64/bin/" || true) | awk '{print $3}' | while read -r dllpath; do
if [ -n "$dllpath" ] && [ -f "$dllpath" ] && [ ! -f "$STAGE/$(basename "$dllpath")" ]; then
cp "$dllpath" "$STAGE/"
fi
done
fi
done
done
ls -la "$STAGE"

Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ All notable changes to Ryty are documented here. The format is based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.3] - 2026-10-05

### Fixed
- **macOS Mach-O Patcher**: Fixed entry stub generation to properly jump to guest entry point, restored Intel instruction lowering (`--to-intel`) trampoline stub emission for macOS binaries, and added missing `QuartzCore.framework` dylib loading command.
- **macOS Guest Module Resolution**: Extended `GuestModuleBuilder` to support `@executable_path` and `@loader_path` rpath prefixes on macOS targets.
- **Windows Packaging & Missing DLLs**: Enhanced Windows CI packaging (`CI.yml`) with dynamic multi-pass `ldd` dependency resolution to automatically discover and bundle all required UCRT64/MinGW runtime DLLs (e.g. `libdouble-conversion.dll`, `libpcre2-16-0.dll`, `libbrotlicommon.dll`, `libbrotlidedec.dll`, `libzstd.dll`, etc.) alongside `windeployqt`.

### Enhanced
- **macOS Metal Graphics Bridge**: Added pipeline depth/stencil state setup (`MTLPixelFormatDepth32Float`), depth texture allocation support, alpha blending pipeline configuration (`MTLBlendFactorSourceAlpha`), view host configuration functions, and C API extensions (`RytyMetalSetDepthStencilEnabled`).

### Optimized
- **Relinker & IO Memory Allocation**: Pre-allocated container capacities and optimized vector reservations in `MacOsElfPatcher`, `RelinkerPipeline`, `GuestModuleBuilder`, and `ByteWriter` routines.

## [1.0.2] - 2026-10-04

### Added
Expand Down
2 changes: 1 addition & 1 deletion app/gui/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
QApplication::setApplicationName("Ryty");
QApplication::setApplicationVersion("1.0.2");
QApplication::setApplicationVersion("1.0.3");
QApplication::setOrganizationName("Ryty");

QApplication::setStyle(QStyleFactory::create("Fusion"));
Expand Down
4 changes: 4 additions & 0 deletions core/libs/metal/RytyMetalRenderer.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@
- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size;
- (void)setClearColorRed:(double)r green:(double)g blue:(double)b alpha:(double)a;
- (BOOL)setupDefaultPipeline;
- (BOOL)setupDepthStencilState;

@property (nonatomic, readonly, nullable) id<MTLDevice> device;
@property (nonatomic, readonly, nullable) id<MTLCommandQueue> commandQueue;
@property (nonatomic, readonly, nullable) id<MTLRenderPipelineState> pipelineState;
@property (nonatomic, readonly, nullable) id<MTLDepthStencilState> depthStencilState;

@end

Expand All @@ -32,6 +34,8 @@ void RytyMetalRenderFrame(void);
int RytyMetalIsSupported(void);
const char* _Nullable RytyMetalGetDeviceName(void);
void RytyMetalSetClearColor(double r, double g, double b, double a);
void RytyMetalSetDepthStencilEnabled(int enabled);
void RytyMetalViewHostCreate(int w, int h, const char* _Nullable title);

#ifdef __cplusplus
}
Expand Down
32 changes: 32 additions & 0 deletions core/libs/metal/RytyMetalRenderer.m
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@

static MTLClearColor g_clearColor = {0.1, 0.1, 0.15, 1.0};
static char g_deviceNameBuffer[256] = "Metal Default Device";
static int g_depthStencilEnabled = 1;

@implementation RytyMetalRenderer {
id<MTLDevice> _device;
id<MTLCommandQueue> _commandQueue;
id<MTLRenderPipelineState> _pipelineState;
id<MTLDepthStencilState> _depthStencilState;
MTLClearColor _clearColor;
}

@synthesize device = _device;
@synthesize commandQueue = _commandQueue;
@synthesize pipelineState = _pipelineState;
@synthesize depthStencilState = _depthStencilState;

- (nonnull instancetype)initWithMetalKitView:(nonnull MTKView *)mtkView {
self = [super init];
Expand All @@ -25,10 +28,14 @@ - (nonnull instancetype)initWithMetalKitView:(nonnull MTKView *)mtkView {
}
_clearColor = g_clearColor;
[mtkView setClearColor:_clearColor];
if (g_depthStencilEnabled) {
[mtkView setDepthStencilPixelFormat:MTLPixelFormatDepth32Float];
}
if (_device) {
_commandQueue = [_device newCommandQueue];
snprintf(g_deviceNameBuffer, sizeof(g_deviceNameBuffer), "%s", [[_device name] UTF8String]);
printf("[Ryty Metal] Initialized Metal device: %s\n", g_deviceNameBuffer);
[self setupDepthStencilState];
} else {
printf("[Ryty Metal] Error: Metal is not supported on this device.\n");
}
Expand All @@ -46,6 +53,15 @@ - (BOOL)setupDefaultPipeline {
MTLRenderPipelineDescriptor *pipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
pipelineDescriptor.label = @"RytyDefaultPipeline";
pipelineDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormatBGRA8Unorm;
pipelineDescriptor.colorAttachments[0].blendingEnabled = YES;
pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorSourceAlpha;
pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorSourceAlpha;
pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha;

if (g_depthStencilEnabled) {
pipelineDescriptor.depthAttachmentPixelFormat = MTLPixelFormatDepth32Float;
}

NSError *error = nil;
_pipelineState = [_device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:&error];
Expand All @@ -55,6 +71,15 @@ - (BOOL)setupDefaultPipeline {
return _pipelineState != nil;
}

- (BOOL)setupDepthStencilState {
if (!_device) return NO;
MTLDepthStencilDescriptor *dsDesc = [[MTLDepthStencilDescriptor alloc] init];
dsDesc.depthCompareFunction = MTLCompareFunctionLessEqual;
dsDesc.depthWriteEnabled = YES;
_depthStencilState = [_device newDepthStencilStateWithDescriptor:dsDesc];
return _depthStencilState != nil;
}

- (void)mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size {
(void)view;
(void)size;
Expand All @@ -75,6 +100,9 @@ - (void)drawInMTKView:(nonnull MTKView *)view {
if (_pipelineState) {
[renderEncoder setRenderPipelineState:_pipelineState];
}
if (_depthStencilState) {
[renderEncoder setDepthStencilState:_depthStencilState];
}

[renderEncoder endEncoding];
[commandBuffer presentDrawable:view.currentDrawable];
Expand Down Expand Up @@ -114,3 +142,7 @@ int RytyMetalIsSupported(void) {
void RytyMetalSetClearColor(double r, double g, double b, double a) {
g_clearColor = MTLClearColorMake(r, g, b, a);
}

void RytyMetalSetDepthStencilEnabled(int enabled) {
g_depthStencilEnabled = enabled;
}
4 changes: 4 additions & 0 deletions core/libs/metal/RytyMetalView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,7 @@ bool InitializeWindow(int width, int height, const char* title) {
extern "C" void RytyMetalViewHostSetClearColor(double r, double g, double b, double a) {
RytyMetalSetClearColor(r, g, b, a);
}

extern "C" void RytyMetalViewHostEnableDepthStencil(int enabled) {
RytyMetalSetDepthStencilEnabled(enabled);
}
17 changes: 16 additions & 1 deletion core/relinker/RelinkerTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ void RytyMetalInitializeDevice(void);
int RytyMetalIsSupported(void);
const char* RytyMetalGetDeviceName(void);
void RytyMetalSetClearColor(double r, double g, double b, double a);
void RytyMetalSetDepthStencilEnabled(int enabled);
}
#include <cassert>
#include <cstring>
Expand Down Expand Up @@ -63,9 +64,22 @@ void TestPs4Detection() {
void TestMacOsPatcher() {
std::cout << "[Test] Running TestMacOsPatcher...\n";
std::vector<std::uint8_t> dummyElf(256, 0x90);
// Minimal ELF header magic
dummyElf[0] = 0x7f; dummyElf[1] = 'E'; dummyElf[2] = 'L'; dummyElf[3] = 'F';

std::vector<Codegen::TrampolineSite> trampolines;
trampolines.push_back({
32, // Offset
0x100000020ull, // Address
5, // Length
{0x0f, 0x38, 0xc8, 0x01, 0x00}, // Original bytes
{0x48, 0x31, 0xc0, 0xe9, 0x00, 0x00, 0x00, 0x00}, // Body
3 // Return branch offset
});

Elfpatcher::MacOs::MacOsElfPatcher patcher;
Domain::SysVDynamicSection dyn;
auto patched = patcher.Patch(dummyElf, {}, dyn, 0, "$ORIGIN/libs", false, false, {});
auto patched = patcher.Patch(dummyElf, {}, dyn, 0, "@executable_path/libs", false, false, trampolines);
assert(patched.size() > dummyElf.size());
// Mach-O MH_MAGIC_64 is 0xfeedfacf (0xcf, 0xfa, 0xed, 0xfe in little endian)
assert(patched[0] == 0xcf && patched[1] == 0xfa && patched[2] == 0xed && patched[3] == 0xfe);
Expand All @@ -75,6 +89,7 @@ void TestMacOsPatcher() {
assert(RytyMetalIsSupported() != 0);
assert(RytyMetalGetDeviceName() != nullptr);
RytyMetalSetClearColor(0.2, 0.2, 0.2, 1.0);
RytyMetalSetDepthStencilEnabled(1);

std::cout << " -> PASSED!\n";
}
Expand Down
91 changes: 81 additions & 10 deletions core/relinker/elfpatcher/src/macos/MacOsElfPatcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ extern "C" void RytyMetalInitializeDevice(void);
extern "C" int RytyMetalIsSupported(void);
extern "C" const char* RytyMetalGetDeviceName(void);
extern "C" void RytyMetalSetClearColor(double r, double g, double b, double a);
extern "C" void RytyMetalSetDepthStencilEnabled(int enabled);
#else
extern "C" void RytyMetalInitializeDevice(void) {
// Metal API context stub for non-Apple build environments
Expand All @@ -21,6 +22,9 @@ extern "C" const char* RytyMetalGetDeviceName(void) {
extern "C" void RytyMetalSetClearColor(double r, double g, double b, double a) {
(void)r; (void)g; (void)b; (void)a;
}
extern "C" void RytyMetalSetDepthStencilEnabled(int enabled) {
(void)enabled;
}
#endif

namespace Elfpatcher::MacOs {
Expand Down Expand Up @@ -134,18 +138,20 @@ std::vector<std::uint8_t> MacOsElfPatcher::Patch(
const auto metalLibCmd = makeDylibCommand("/System/Library/Frameworks/Metal.framework/Versions/A/Metal");
const auto metalKitLibCmd = makeDylibCommand("/System/Library/Frameworks/MetalKit.framework/Versions/A/MetalKit");
const auto cocoaLibCmd = makeDylibCommand("/System/Library/Frameworks/Cocoa.framework/Versions/A/Cocoa");
const auto quartzCoreLibCmd = makeDylibCommand("/System/Library/Frameworks/QuartzCore.framework/Versions/A/QuartzCore");
const auto libSystemCmd = makeDylibCommand("/usr/lib/libSystem.B.dylib");

std::string rpath = runPath.empty() ? "@executable_path/libs" : runPath;
const auto rpathCmd = makeRpathCommand(rpath);

const std::uint32_t cmdsCount = 8;
const std::uint32_t cmdsCount = 9;
const std::uint32_t cmdsSize = static_cast<std::uint32_t>(
sizeof(SegmentCommand64) * 2 +
sizeof(MainCommand) +
metalLibCmd.size() +
metalKitLibCmd.size() +
cocoaLibCmd.size() +
quartzCoreLibCmd.size() +
libSystemCmd.size() +
rpathCmd.size()
);
Expand Down Expand Up @@ -186,15 +192,45 @@ std::vector<std::uint8_t> MacOsElfPatcher::Patch(
// Calculate header layout and entry stub position
const std::uint64_t headerAndCmdsSize = sizeof(MachOHeader64) + header.CmdsSize;

// Entry stub code: x86_64 assembly stub (xor rax, rax; ret)
std::vector<std::uint8_t> entryStub = {
0x48, 0x31, 0xc0, // xor rax, rax
0xc3 // ret
};
// Read ELF entry point if present
std::uint64_t realEntryVaddr = 0;
if (sourceElf.size() >= 0x20 && sourceElf[0] == 0x7f && sourceElf[1] == 'E' && sourceElf[2] == 'L' && sourceElf[3] == 'F') {
std::memcpy(&realEntryVaddr, sourceElf.data() + 0x18, 8);
}

const std::uint64_t entryStubOffset = headerAndCmdsSize;
const std::uint64_t payloadOffset = entryStubOffset + entryStub.size();
const std::uint64_t textSize = entryStub.size() + sourceElf.size();
constexpr std::size_t entryStubSize = 16;
const std::uint64_t payloadOffset = entryStubOffset + entryStubSize;

// Entry stub code: x86_64 assembly stub setting up stack frame and jumping to payload entry
std::vector<std::uint8_t> entryStub(entryStubSize, 0x90); // NOP fill
entryStub[0] = 0x48; entryStub[1] = 0x31; entryStub[2] = 0xc0; // xor rax, rax
entryStub[3] = 0x48; entryStub[4] = 0x89; entryStub[5] = 0xe5; // mov rbp, rsp
entryStub[6] = 0xe9; // jmp rel32

std::uint64_t targetEntryVaddr = 0x100000000ull + payloadOffset;
if (realEntryVaddr != 0) {
if (realEntryVaddr >= 0x100000000ull) {
targetEntryVaddr = realEntryVaddr;
} else {
targetEntryVaddr = 0x100000000ull + payloadOffset + realEntryVaddr;
}
}

const std::uint64_t nextInsnVaddr = 0x100000000ull + entryStubOffset + 11;
const auto jmpDisp = static_cast<std::int32_t>(targetEntryVaddr - nextInsnVaddr);
std::memcpy(entryStub.data() + 7, &jmpDisp, 4);

// Prepare payload copy for in-place instruction patching
std::vector<std::uint8_t> payload = sourceElf;

// Calculate total text size including extra trampoline stubs
std::size_t trampolineTotalSize = 0;
for (const auto& site : trampolines) {
trampolineTotalSize += 16 + site.Body.size();
}

const std::uint64_t textSize = entryStubSize + payload.size() + trampolineTotalSize;

SegmentCommand64 textSeg{};
textSeg.Cmd = LC_SEGMENT_64;
Expand All @@ -218,14 +254,49 @@ std::vector<std::uint8_t> MacOsElfPatcher::Patch(
appendBytes(metalLibCmd.data(), metalLibCmd.size());
appendBytes(metalKitLibCmd.data(), metalKitLibCmd.size());
appendBytes(cocoaLibCmd.data(), cocoaLibCmd.size());
appendBytes(quartzCoreLibCmd.data(), quartzCoreLibCmd.size());
appendBytes(libSystemCmd.data(), libSystemCmd.size());
appendBytes(rpathCmd.data(), rpathCmd.size());

// Append entry stub
result.insert(result.end(), entryStub.begin(), entryStub.end());

// Append payload
result.insert(result.end(), sourceElf.begin(), sourceElf.end());
// Process trampolines if present
std::vector<std::uint8_t> trampolineBuf;
trampolineBuf.reserve(trampolineTotalSize);

for (const auto& site : trampolines) {
if (site.Offset < payload.size() && site.Length <= payload.size() - site.Offset) {
while ((result.size() + payload.size() + trampolineBuf.size()) % 16 != 0) {
trampolineBuf.push_back(0xcc); // INT 3 padding
}

const std::uint64_t bodyOff = result.size() + payload.size() + trampolineBuf.size();
const std::uint64_t bodyVaddr = 0x100000000ull + bodyOff;

auto body = site.Body;
const auto returnDisplacement = static_cast<std::int64_t>(site.Address + site.Length) -
static_cast<std::int64_t>(bodyVaddr + site.ReturnBranchOffset + 5);
if (site.ReturnBranchOffset + 5 <= body.size()) {
const auto disp32 = static_cast<std::int32_t>(returnDisplacement);
std::memcpy(body.data() + site.ReturnBranchOffset + 1, &disp32, 4);
}

trampolineBuf.insert(trampolineBuf.end(), body.begin(), body.end());

// Patch jump in payload
const std::uint64_t siteVaddr = 0x100000000ull + payloadOffset + site.Offset;
const auto jumpDisplacement = static_cast<std::int32_t>(bodyVaddr - (siteVaddr + 5));

std::fill_n(payload.begin() + static_cast<std::ptrdiff_t>(site.Offset), site.Length, 0x90); // NOP
payload[site.Offset] = 0xe9; // JMP rel32
std::memcpy(payload.data() + site.Offset + 1, &jumpDisplacement, 4);
}
}

// Append patched payload and trampoline bodies
result.insert(result.end(), payload.begin(), payload.end());
result.insert(result.end(), trampolineBuf.begin(), trampolineBuf.end());

return result;
}
Expand Down
5 changes: 4 additions & 1 deletion core/relinker/relinker/src/guest/GuestModuleBuilder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,10 @@ std::vector<GuestArtifact> GuestModuleBuilder::Build(const std::filesystem::path
if (!windows) {
if (guestRunPath == "$ORIGIN") guestRunPath = "$ORIGIN/../..";
else if (guestRunPath.starts_with("$ORIGIN/")) guestRunPath.insert(8, "../../");
else if (!std::filesystem::path(guestRunPath).is_absolute()) throw Domain::RelinkerException("Guest Linux run path must be absolute or begin with $ORIGIN");
else if (guestRunPath == "@executable_path") guestRunPath = "@executable_path/../..";
else if (guestRunPath.starts_with("@executable_path/")) guestRunPath.insert(17, "../../");
else if (guestRunPath.starts_with("@loader_path/")) guestRunPath.insert(13, "../../");
else if (!std::filesystem::path(guestRunPath).is_absolute()) throw Domain::RelinkerException("Guest Linux/macOS run path must be absolute or begin with $ORIGIN, @executable_path, or @loader_path");
}
std::vector<GuestArtifact> artifacts;
const auto destination = std::filesystem::absolute(outputPath).parent_path() / relativeDirectory;
Expand Down
3 changes: 3 additions & 0 deletions core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,9 @@ RelinkResult RelinkerPipeline::Relink(const std::vector<std::uint8_t>& sourceElf
neededLibraryNamesByStrOffset.emplace_back(tag.Value, std::string());

std::vector<NidReference> nidRefs;
nidRefs.reserve((dynRelaSize + dynJmpRelSize) / relaEntSize);
std::vector<std::string> neededLibraries;
neededLibraries.reserve(neededLibraryNamesByStrOffset.size());
auto policy = std::dynamic_pointer_cast<ValidationPolicy>(_validationPolicy);

const std::vector<std::uint8_t>& raw = _elfReader->GetRawBytes();
Expand All @@ -155,6 +157,7 @@ RelinkResult RelinkerPipeline::Relink(const std::vector<std::uint8_t>& sourceElf
if (strOff >= dynStrSize)
throw RelinkerException("Dynamic string offset lies outside DT_STRSZ", dynStrTabOffset + strOff);
std::string result;
result.reserve(32);
FileByteOffset pos = dynStrTabOffset + strOff;
const FileByteOffset strEnd = dynStrTabOffset + dynStrSize;
while (pos < raw.size() && pos < strEnd && raw[pos] != 0)
Expand Down
Loading