Skip to content
Open
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
16 changes: 16 additions & 0 deletions Objects/file formats/XGAssemblyCodeExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,22 @@ extension XGAssembly {
}
#endif

class func replaceRamASM(RAMOffset: Int, newASM asm: [UInt32]) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to keep this? I think I removed it and switched to the metroworks area in dol as the preference. This could still be useful in future if we end up writing a lot of assembly code. It's region specific though which is the main reason I didn't like it as a solution.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually just the UInt32 version of the function, they are identical with different return types

var offset = RAMOffset
if offset > 0x80000000 {
offset -= 0x80000000
}
#if GAME_PBR
replaceASM(startOffset: offset - kDolToRAMOffsetDifference, newASM: asm)
#else
if game == .XD, region == .US, offset > kRELtoRAMOffsetDifference {
replaceRELASM(startOffset: offset - kRELtoRAMOffsetDifference, newASM: asm)
} else {
replaceASM(startOffset: offset - kDolToRAMOffsetDifference, newASM: asm)
}
#endif
}

class func replaceRamASM(RAMOffset: Int, newASM asm: [XGASM]) {
var offset = RAMOffset
if offset > 0x80000000 {
Expand Down
55 changes: 10 additions & 45 deletions Objects/managers/GoDShellManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ class GoDShellManager {
enum Commands {
case wit
case wimgt
case gcc
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The indentation is a bit funny here but no big deal

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whoops

case ld
case nm
case objdump
case objcopy
case gcitool
case gcitoolReplace
case pbrSaveTool
Expand All @@ -21,6 +26,11 @@ class GoDShellManager {
switch self {
case .wit: return .wit
case .wimgt: return .wimgt
case .gcc: return .gcc
case .ld: return .ld
case .nm: return .nm
case .objdump: return .objdump
case .objcopy: return .objcopy
case .gcitool: return .tool("gcitool")
case .gcitoolReplace: return .tool("gcitool_replace")
case .pbrSaveTool: return .tool("pbrsavetool")
Expand Down Expand Up @@ -79,49 +89,4 @@ class GoDShellManager {
return output
}

@discardableResult
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean to remove this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, my bad!

static func runAsync(_ command: Commands, args: String? = nil, inputRedirectFile: XGFiles? = nil, outputRedirectFile: XGFiles? = nil, errorRedirectFile: XGFiles? = nil) -> GoDProcess? {
guard command.file.exists else {
printg("command, \(command.file.fileName), doesn't exist\n\(command.file.path) not found")
return nil
}

let process = Process()
process.executableURL = URL(fileURLWithPath: command.file.path)
if let args = args {
let escaped = args.replacingOccurrences(of: "\\ ", with: "<!SPACE>")
process.arguments = escaped.split(separator: " ").compactMap(String.init).map({ (arg) -> String in
return arg.replacingOccurrences(of: "<!SPACE>", with: " ")
})
}

if let inputFile = inputRedirectFile, inputFile.exists {
let fileHandle = FileHandle(forReadingAtPath: inputFile.path)
process.standardInput = fileHandle
}

if let outputFile = outputRedirectFile {
let fileHandle = FileHandle(forWritingAtPath: outputFile.path)
process.standardOutput = fileHandle
} else {
process.standardOutput = nil
}

if let errorFile = errorRedirectFile {
let fileHandle = FileHandle(forWritingAtPath: errorFile.path)
process.standardError = fileHandle
} else {
process.standardError = nil
}

do {
try process.run()
return GoDProcess(process: process)
} catch let error {
printg("Shell error:", error)
}
return nil
}


}
272 changes: 272 additions & 0 deletions Objects/managers/XGCompiler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
import Foundation

enum XGCompilerTarget {
case ogc
case bare
}

class XGCompiler: NSObject {
class func compileBinary(startOffset: Int, type: XGCompilerTarget = .bare, sources: [String], linkerScripts: [String], directoryPath: String) -> (instructions: [UInt32], exports: [String:UInt32], patches: [UInt32:UInt32]) {
var offset = startOffset
if offset < 0x80000000 {
offset += 0x80000000
}

let directory = URL(fileURLWithPath: directoryPath)

let compiledFileURL = directory.appendingPathComponent("code.elf")
try? FileManager.default.removeItem(at: compiledFileURL)

var args = sources.map{ directory.appendingPathComponent($0).path.escapedPath }.joined(separator: " ")
args += " -o \(compiledFileURL.path.escapedPath)"
args += " -I\(directoryPath.escapedPath)"
args += " -O1 -mcpu=750 -meabi -mhard-float"
args += " -mno-sdata -Wno-builtin-declaration-mismatch"
args += " -ffast-math -flto -fwhole-program -fdata-sections -fkeep-static-functions"
args += " -fno-tree-loop-distribute-patterns -fno-zero-initialized-in-bss"
args += " -Wl,-Map=\(directory.appendingPathComponent("code.map").path.escapedPath)"
args += linkerScripts.map {
var path = directory.appendingPathComponent($0).path.escapedPath
if $0.prefix(1) == "/" {
path = $0.escapedPath
}
return " -T \(path)"
}.joined(separator: " ")
switch type {
case .ogc:
args += " -I/opt/devkitpro/libogc/include -DGEKKO -mogc"
args += " -L/opt/devkitpro/libogc/lib/cube -ldb -lbba -lmad -lasnd -logc -lm"
args += " -Wl,--gc-sections -Wl,--section-start,.init=\(offset.hexString())"
case .bare:
args += " -nolibc -nostdlib -nodefaultlibs"
args += " -Wl,--gc-sections -Wl,--section-start,.text=\(offset.hexString())"
}

// print(args)
GoDShellManager.run(.gcc, args: args, printOutput: false)

var out = GoDShellManager.run(.objcopy, args: "--remove-section .comment \(compiledFileURL.path.escapedPath)", printOutput: false)
print(out!.dropLast())

out = GoDShellManager.run(.nm, args: "-a \(compiledFileURL.path.escapedPath)", printOutput: false)
let exports = XGCompiler.getExports(out!)
let imports = XGCompiler.getImports(out!)

let patches = XGCompiler.getPatches(sources.map{ directory.appendingPathComponent($0).path }, imports: imports, exports: exports)

if settings.verbose {
out = GoDShellManager.run(.objdump, args: "-D \(compiledFileURL.path.escapedPath)", printOutput: false)
printg("generated asm: \(out!.dropLast())")
}

let codeBinaryFileURL = directory.appendingPathComponent("code.bin")
GoDShellManager.run(.objcopy, args: "-O binary \(compiledFileURL.path.escapedPath) \(codeBinaryFileURL.path.escapedPath)", printOutput: false)

guard var codeBinaryData = try? Data(contentsOf: codeBinaryFileURL).rawBytes else {
fatalError("Could not read output compilation output")
}

// pad to the nearest word
let words = codeBinaryData.count / 4
let rem = codeBinaryData.count - (words * 4)
let padding = 4 - rem
codeBinaryData.append(contentsOf: [UInt8](repeating: 0, count: padding))

let instructions = codeBinaryData.withUnsafeBytes {
Array($0.bindMemory(to: UInt32.self)).map(UInt32.init(bigEndian:))
}

return (instructions, exports, patches)
}

class func compileCode(startOffset: Int, code: String, include: String = "", externSymbols: [String:UInt32] = [:]) -> (instructions: [UInt32], exports: [String:UInt32], patches: [UInt32:UInt32]) {
let funcName = "__func"

var offset = startOffset
if offset < 0x80000000 {
offset += 0x80000000
}

let tmpDir = try? XGTemporaryDirectory()
// tmpDir!.keepDirectory = true
let tmpDirPath = tmpDir!.directoryURL.path
let folder = XGFolders.path(tmpDirPath)

let includeFile = XGFiles.nameAndFolder("code.h", folder)
XGUtility.saveString(XGCompilerConstants.dolphinIncludes+include, toFile: includeFile)

let codeFile = XGFiles.nameAndFolder("code.c", folder)
XGUtility.saveString("""
#include "code.h"
void \(funcName)() {
\(code)
}
""", toFile: codeFile)

let linkFile = XGFiles.nameAndFolder("ngc.ld", folder)
XGUtility.saveString(generateLinkerScript(offset: offset, name: funcName), toFile: linkFile)

let mapFile = XGFiles.nameAndFolder("map.ld", folder)
XGUtility.saveString(generateMapScript(), toFile: mapFile)

let externFile = XGFiles.nameAndFolder("extern.ld", folder)
XGUtility.saveString(generateExternScript(externSymbols), toFile: externFile)

return XGCompiler.compileBinary(startOffset: startOffset, type: .bare, sources: ["code.c"], linkerScripts: ["ngc.ld", "map.ld", "external.ld"], directoryPath: tmpDirPath)
}

class func getPatches(_ sources: [String], imports: [String:UInt32], exports: [String:UInt32]) -> [UInt32:UInt32] {
var patches : [UInt32:UInt32] = [:]

let pattern = "^PATCH_CALL\\((.*), (.*)\\)"
let regex = try! NSRegularExpression(pattern: pattern)

for source in sources {
let contents = try! String(contentsOf: URL(fileURLWithPath: source))
let lines = contents.components(separatedBy: CharacterSet.newlines)

for line in lines {
let lineRange = NSRange(location: 0, length: line.utf16.count)
guard let match = regex.firstMatch(in: line, options: [], range: lineRange) else { continue }

let locationRange = Range(match.range(at: 1), in: line)!
let location = String(line[locationRange])

var address : UInt32
if let val = imports[location] {
address = val
} else {
address = UInt32(location.dropFirst(2), radix: 16)!
}

let symbolRange = Range(match.range(at: 2), in: line)!
let symbol = String(line[symbolRange])

let code = exports[symbol]!

patches[address] = code
}

}

return patches
}

class func applyPatches(_ patches: [UInt32:UInt32]) {
for (absCallLocation, absCallTarget) in patches {
let callLocation = Int(absCallLocation) - 0x80000000
let callTarget = Int(absCallTarget) - 0x80000000
print("Patching function call at \(absCallLocation.hexString())")
XGAssembly.replaceRamASM(RAMOffset: callLocation, newASM: [.bl(callTarget)])
}
}

class func getExports(_ text: String) -> [String:UInt32] {
return XGCompiler.parseNameMangling(text, include: ["T", "t", "d"])
}

class func getImports(_ text: String) -> [String:UInt32] {
return XGCompiler.parseNameMangling(text, include: ["A"])
}

class func parseNameMangling(_ text: String, include: [Character]) -> [String:UInt32] {
let lines = text.components(separatedBy: CharacterSet.newlines)

var symbols: [String:UInt32] = [:]

for line in lines {
// skip empty lines and unnamed symbols
if line.length == 0 || line.components(separatedBy: .whitespacesAndNewlines).filter({!$0.isEmpty}).count != 3 {
continue
}
let scanner = Scanner(string: line)

let startAddress = UInt32(scanner.scanUInt64(representation: .hexadecimal)!)
let symbolType = scanner.scanCharacter()!
let symbolName = scanner.scanUpToString("\n")!

if !include.contains(symbolType) {
continue
}
symbols[symbolName] = startAddress
}

return symbols
}

class func getDolphinMap() -> String {
let appSupportURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
let appURLs = try? FileManager.default.contentsOfDirectory(at: appSupportURL!, includingPropertiesForKeys: nil)
let appURL = appURLs!.filter{ ["Dolphin", "dolphin-emu"].contains($0.lastPathComponent) }.first
return appURL!.appendingPathComponent("Maps/GXXE01.map").path
}

class func parseDolphinMap() -> [String:UInt32] {
let path = XGCompiler.getDolphinMap()
let text = try! String(contentsOfFile: path, encoding: String.Encoding.utf8)
let lines = text.components(separatedBy: CharacterSet.newlines)

var symbols: [String:UInt32] = [:]

for line in lines {
if line.first == "." || line.count == 0 {
continue
}
let charset = CharacterSet(charactersIn: ":/<>,'%?&")
if line.rangeOfCharacter(from: charset) != nil {
continue
}

let scanner = Scanner(string: line)

let startAddress = UInt32(scanner.scanUInt64(representation: .hexadecimal)!)
let _ /* symbolLength */ = scanner.scanUInt64(representation: .hexadecimal)!
let _ /* virtualAddress */ = scanner.scanUInt64(representation: .hexadecimal)!
let _ /* number */ = scanner.scanInt(representation: .decimal)!
let symbolName = String(scanner.string[scanner.currentIndex...])

symbols[symbolName] = startAddress
}

return symbols
}

class func checkForCompiler() throws {

}

class func generateExternScript(_ externSymbols: [String:UInt32] = [:]) -> String {
var symbols = [String]()
for (symbol, address) in externSymbols {
symbols.append("\(symbol) = \(address.hexString());")
}

return symbols.joined(separator: "\n")
}

class func generateMapScript() -> String {
let symbolMap = parseDolphinMap()
return generateExternScript(symbolMap)
}

class func generateLinkerScript(offset: Int, name: String) -> String {
return """
OUTPUT_FORMAT("elf32-powerpc", "elf32-powerpc", "elf32-powerpc")
OUTPUT_ARCH(powerpc:common)
ENTRY(\(name))

SECTIONS {
. = \(offset.hexString());

.text : {
*(.text)
} =0

. = ALIGN(32);
.data : {
SORT(CONSTRUCTORS)
}
}
"""
}
}
Loading