Merge branch 'master' into delete-emu

This commit is contained in:
wheremyfoodat 2024-02-01 19:16:04 +02:00
commit 8cee60ebf5
286 changed files with 13182 additions and 508 deletions

View file

@ -11,13 +11,15 @@
// We are legally allowed, as per the author's wish, to use the above code without any licensing restrictions
// However we still want to follow the license as closely as possible and offer the proper attributions.
EmulatorConfig::EmulatorConfig(const std::filesystem::path& path) { load(path); }
EmulatorConfig::EmulatorConfig(const std::filesystem::path& path) : filePath(path) { load(); }
void EmulatorConfig::load() {
const std::filesystem::path& path = filePath;
void EmulatorConfig::load(const std::filesystem::path& path) {
// If the configuration file does not exist, create it and return
std::error_code error;
if (!std::filesystem::exists(path, error)) {
save(path);
save();
return;
}
@ -56,7 +58,7 @@ void EmulatorConfig::load(const std::filesystem::path& path) {
rendererType = RendererType::OpenGL;
}
shaderJitEnabled = toml::find_or<toml::boolean>(gpu, "EnableShaderJIT", true);
shaderJitEnabled = toml::find_or<toml::boolean>(gpu, "EnableShaderJIT", shaderJitDefault);
}
}
@ -84,8 +86,9 @@ void EmulatorConfig::load(const std::filesystem::path& path) {
}
}
void EmulatorConfig::save(const std::filesystem::path& path) {
void EmulatorConfig::save() {
toml::basic_value<toml::preserve_comments> data;
const std::filesystem::path& path = filePath;
std::error_code error;
if (std::filesystem::exists(path, error)) {

View file

@ -1,32 +1,59 @@
#ifdef CPU_DYNARMIC
#include "cpu_dynarmic.hpp"
#include "arm_defs.hpp"
#include "emulator.hpp"
CPU::CPU(Memory& mem, Kernel& kernel) : mem(mem), env(mem, kernel) {
cp15 = std::make_shared<CP15>();
CPU::CPU(Memory& mem, Kernel& kernel, Emulator& emu) : mem(mem), emu(emu), scheduler(emu.getScheduler()), env(mem, kernel, emu.getScheduler()) {
cp15 = std::make_shared<CP15>();
Dynarmic::A32::UserConfig config;
config.arch_version = Dynarmic::A32::ArchVersion::v6K;
config.callbacks = &env;
config.coprocessors[15] = cp15;
config.define_unpredictable_behaviour = true;
config.global_monitor = &exclusiveMonitor;
config.processor_id = 0;
jit = std::make_unique<Dynarmic::A32::Jit>(config);
Dynarmic::A32::UserConfig config;
config.arch_version = Dynarmic::A32::ArchVersion::v6K;
config.callbacks = &env;
config.coprocessors[15] = cp15;
config.define_unpredictable_behaviour = true;
config.global_monitor = &exclusiveMonitor;
config.processor_id = 0;
jit = std::make_unique<Dynarmic::A32::Jit>(config);
}
void CPU::reset() {
setCPSR(CPSR::UserMode);
setFPSCR(FPSCR::MainThreadDefault);
env.totalTicks = 0;
setCPSR(CPSR::UserMode);
setFPSCR(FPSCR::MainThreadDefault);
env.totalTicks = 0;
cp15->reset();
cp15->setTLSBase(VirtualAddrs::TLSBase); // Set cp15 TLS pointer to the main thread's thread-local storage
jit->Reset();
jit->ClearCache();
jit->Regs().fill(0);
jit->ExtRegs().fill(0);
cp15->reset();
cp15->setTLSBase(VirtualAddrs::TLSBase); // Set cp15 TLS pointer to the main thread's thread-local storage
jit->Reset();
jit->ClearCache();
jit->Regs().fill(0);
jit->ExtRegs().fill(0);
}
#endif // CPU_DYNARMIC
void CPU::runFrame() {
emu.frameDone = false;
while (!emu.frameDone) {
// Run CPU until the next scheduler event
env.ticksLeft = scheduler.nextTimestamp - scheduler.currentTimestamp;
execute:
const auto exitReason = jit->Run();
// Handle any scheduler events that need handling.
emu.pollScheduler();
if (static_cast<u32>(exitReason) != 0) [[unlikely]] {
// Cache invalidation needs to exit the JIT so it returns a CacheInvalidation HaltReason. In our case, we just go back to executing
// The goto might be terrible but it does guarantee that this does not recursively call run and crash, instead getting optimized to a jump
if (Dynarmic::Has(exitReason, Dynarmic::HaltReason::CacheInvalidation)) {
goto execute;
} else {
Helpers::panic("Exit reason: %d\nPC: %08X", static_cast<u32>(exitReason), getReg(15));
}
}
}
}
#endif // CPU_DYNARMIC

View file

@ -0,0 +1,947 @@
#if defined(PANDA3DS_DYNAPICA_SUPPORTED) && defined(PANDA3DS_ARM64_HOST)
#include "PICA/dynapica/shader_rec_emitter_arm64.hpp"
#include <bit>
using namespace Helpers;
using namespace oaknut;
using namespace oaknut::util;
// Similar to the x64 recompiler, we use an odd internal ABI, which abuses the fact that we'll very rarely be calling C++ functions
// So to avoid pushing and popping, we'll be making use of volatile registers as much as possible
static constexpr QReg scratch1 = Q0;
static constexpr QReg scratch2 = Q1;
static constexpr QReg src1_vec = Q2;
static constexpr QReg src2_vec = Q3;
static constexpr QReg src3_vec = Q4;
static constexpr QReg onesVector = Q5;
static constexpr XReg arg1 = X0;
static constexpr XReg arg2 = X1;
static constexpr XReg statePointer = X9;
void ShaderEmitter::compile(const PICAShader& shaderUnit) {
oaknut::CodeBlock::unprotect(); // Unprotect the memory before writing to it
// Constants
align(16);
// Generate blending masks for doing masked writes to registers
l(blendMasks);
for (int i = 0; i < 16; i++) {
dw((i & 0x8) ? 0xFFFFFFFF : 0); // Mask for x component
dw((i & 0x4) ? 0xFFFFFFFF : 0); // Mask for y component
dw((i & 0x2) ? 0xFFFFFFFF : 0); // Mask for z component
dw((i & 0x1) ? 0xFFFFFFFF : 0); // Mask for w component
}
// Emit prologue first
oaknut::Label prologueLabel;
align(16);
l(prologueLabel);
prologueCb = prologueLabel.ptr<PrologueCallback>();
// Set state pointer to the proper pointer
// state pointer is volatile, no need to preserve it
MOV(statePointer, arg1);
// Generate a vector of all 1.0s for SLT/SGE/RCP/RSQ
FMOV(onesVector.S4(), FImm8(0x70));
// Push a return guard on the stack. This happens due to the way we handle the PICA callstack, by pushing the return PC to stack
// By pushing -1, we make it impossible for a return check to erroneously pass
MOV(arg1, 0xffffffffffffffffll);
// Backup link register (X30) and push return guard
STP(arg1, X30, SP, PRE_INDEXED, -16);
// Jump to code with a tail call
BR(arg2);
// Scan the code for call, exp2, log2, etc instructions which need some special care
// After that, emit exp2 and log2 functions if the corresponding instructions are present
scanCode(shaderUnit);
if (codeHasExp2) Helpers::panic("arm64 shader JIT: Code has exp2");
if (codeHasLog2) Helpers::panic("arm64 shader JIT: Code has log2");
align(16);
// Compile every instruction in the shader
// This sounds horrible but the PICA instruction memory is tiny, and most of the time it's padded wtih nops that compile to nothing
recompilerPC = 0;
loopLevel = 0;
compileUntil(shaderUnit, PICAShader::maxInstructionCount);
// Protect the memory and invalidate icache before executing the code
oaknut::CodeBlock::protect();
oaknut::CodeBlock::invalidate_all();
}
void ShaderEmitter::scanCode(const PICAShader& shaderUnit) {
returnPCs.clear();
for (u32 i = 0; i < PICAShader::maxInstructionCount; i++) {
const u32 instruction = shaderUnit.loadedShader[i];
const u32 opcode = instruction >> 26;
if (isCall(instruction)) {
const u32 num = instruction & 0xff;
const u32 dest = getBits<10, 12>(instruction);
const u32 returnPC = num + dest; // Add them to get the return PC
returnPCs.push_back(returnPC);
} else if (opcode == ShaderOpcodes::EX2) {
codeHasExp2 = true;
} else if (opcode == ShaderOpcodes::LG2) {
codeHasLog2 = true;
}
}
// Sort return PCs so they can be binary searched
std::sort(returnPCs.begin(), returnPCs.end());
}
void ShaderEmitter::compileUntil(const PICAShader& shaderUnit, u32 end) {
while (recompilerPC < end) {
compileInstruction(shaderUnit);
}
}
void ShaderEmitter::compileInstruction(const PICAShader& shaderUnit) {
// Write current location to label for this instruction
l(instructionLabels[recompilerPC]);
// See if PC is a possible return PC and emit the proper code if so
if (std::binary_search(returnPCs.begin(), returnPCs.end(), recompilerPC)) {
Label skipReturn;
LDP(X0, XZR, SP); // W0 = Next return address
MOV(W1, recompilerPC); // W1 = Current PC
CMP(W0, W1); // If they're equal, execute a RET, otherwise skip it
B(NE, skipReturn);
RET();
l(skipReturn);
}
// Fetch instruction and inc PC
const u32 instruction = shaderUnit.loadedShader[recompilerPC++];
const u32 opcode = instruction >> 26;
switch (opcode) {
case ShaderOpcodes::ADD: recADD(shaderUnit, instruction); break;
case ShaderOpcodes::CALL: recCALL(shaderUnit, instruction); break;
case ShaderOpcodes::CALLC: recCALLC(shaderUnit, instruction); break;
case ShaderOpcodes::CALLU: recCALLU(shaderUnit, instruction); break;
case ShaderOpcodes::CMP1:
case ShaderOpcodes::CMP2: recCMP(shaderUnit, instruction); break;
case ShaderOpcodes::DP3: recDP3(shaderUnit, instruction); break;
case ShaderOpcodes::DP4: recDP4(shaderUnit, instruction); break;
// case ShaderOpcodes::DPH:
// case ShaderOpcodes::DPHI: recDPH(shaderUnit, instruction); break;
case ShaderOpcodes::END: recEND(shaderUnit, instruction); break;
// case ShaderOpcodes::EX2: recEX2(shaderUnit, instruction); break;
case ShaderOpcodes::FLR: recFLR(shaderUnit, instruction); break;
case ShaderOpcodes::IFC: recIFC(shaderUnit, instruction); break;
case ShaderOpcodes::IFU: recIFU(shaderUnit, instruction); break;
case ShaderOpcodes::JMPC: recJMPC(shaderUnit, instruction); break;
case ShaderOpcodes::JMPU: recJMPU(shaderUnit, instruction); break;
// case ShaderOpcodes::LG2: recLG2(shaderUnit, instruction); break;
case ShaderOpcodes::LOOP: recLOOP(shaderUnit, instruction); break;
case ShaderOpcodes::MOV: recMOV(shaderUnit, instruction); break;
case ShaderOpcodes::MOVA: recMOVA(shaderUnit, instruction); break;
case ShaderOpcodes::MAX: recMAX(shaderUnit, instruction); break;
case ShaderOpcodes::MIN: recMIN(shaderUnit, instruction); break;
case ShaderOpcodes::MUL: recMUL(shaderUnit, instruction); break;
case ShaderOpcodes::NOP: break;
case ShaderOpcodes::RCP: recRCP(shaderUnit, instruction); break;
case ShaderOpcodes::RSQ: recRSQ(shaderUnit, instruction); break;
// Unimplemented opcodes that don't seem to actually be used but exist in the binary
// EMIT/SETEMIT are used in geometry shaders, however are sometimes found in vertex shaders?
case ShaderOpcodes::EMIT:
case ShaderOpcodes::SETEMIT: log("[ShaderJIT] Unimplemented PICA opcode: %02X\n", opcode); break;
case ShaderOpcodes::BREAK:
case ShaderOpcodes::BREAKC: Helpers::warn("[Shader JIT] Unimplemented BREAK(C) instruction!"); break;
// We consider both MAD and MADI to be the same instruction and decode which one we actually have in recMAD
case 0x30:
case 0x31:
case 0x32:
case 0x33:
case 0x34:
case 0x35:
case 0x36:
case 0x37:
case 0x38:
case 0x39:
case 0x3A:
case 0x3B:
case 0x3C:
case 0x3D:
case 0x3E:
case 0x3F: recMAD(shaderUnit, instruction); break;
case ShaderOpcodes::SLT:
case ShaderOpcodes::SLTI: recSLT(shaderUnit, instruction); break;
case ShaderOpcodes::SGE:
case ShaderOpcodes::SGEI: recSGE(shaderUnit, instruction); break;
default: Helpers::panic("Shader JIT: Unimplemented PICA opcode %X", opcode);
}
}
const ShaderEmitter::vec4f& ShaderEmitter::getSourceRef(const PICAShader& shader, u32 src) {
if (src < 0x10)
return shader.inputs[src];
else if (src < 0x20)
return shader.tempRegisters[src - 0x10];
else if (src <= 0x7f)
return shader.floatUniforms[src - 0x20];
else {
Helpers::warn("[Shader JIT] Unimplemented source value: %X\n", src);
return shader.dummy;
}
}
const ShaderEmitter::vec4f& ShaderEmitter::getDestRef(const PICAShader& shader, u32 dest) {
if (dest < 0x10) {
return shader.outputs[dest];
} else if (dest < 0x20) {
return shader.tempRegisters[dest - 0x10];
}
Helpers::panic("[Shader JIT] Unimplemented dest: %X", dest);
}
// See shader.hpp header for docs on how the swizzle and negate works
template <int sourceIndex>
void ShaderEmitter::loadRegister(QReg dest, const PICAShader& shader, u32 src, u32 index, u32 operandDescriptor) {
u32 compSwizzle; // Component swizzle pattern for the register
bool negate; // If true, negate all lanes of the register
if constexpr (sourceIndex == 1) { // SRC1
negate = (getBit<4>(operandDescriptor)) != 0;
compSwizzle = getBits<5, 8>(operandDescriptor);
} else if constexpr (sourceIndex == 2) { // SRC2
negate = (getBit<13>(operandDescriptor)) != 0;
compSwizzle = getBits<14, 8>(operandDescriptor);
} else if constexpr (sourceIndex == 3) { // SRC3
negate = (getBit<22>(operandDescriptor)) != 0;
compSwizzle = getBits<23, 8>(operandDescriptor);
}
// TODO: Do indexes get applied if src < 0x20?
switch (index) {
case 0:
[[likely]] { // Keep src as is, no need to offset it
const vec4f& srcRef = getSourceRef(shader, src);
const uintptr_t offset = uintptr_t(&srcRef) - uintptr_t(&shader); // Calculate offset of register from start of the state struct
LDR(dest, statePointer, offset);
switch (compSwizzle) {
case noSwizzle: break; // .xyzw
case 0x0: DUP(dest.S4(), dest.Selem()[0]); break; // .xxxx
case 0x55: DUP(dest.S4(), dest.Selem()[1]); break; // .yyyy
case 0xAA: DUP(dest.S4(), dest.Selem()[2]); break; // .zzzz
case 0xFF:
DUP(dest.S4(), dest.Selem()[3]);
break; // .wwww
// Some of these cases may still be optimizable
default: {
MOV(scratch1.B16(), dest.B16()); // Make a copy of the register
const auto newX = getBits<6, 2>(compSwizzle);
const auto newY = getBits<4, 2>(compSwizzle);
const auto newZ = getBits<2, 2>(compSwizzle);
const auto newW = getBits<0, 2>(compSwizzle);
// If the lane swizzled into the new x component is NOT the current x component, swizzle the correct lane with a mov
// Repeat for each component of the vector
if (newX != 0) {
MOV(dest.Selem()[0], scratch1.Selem()[newX]);
}
if (newY != 1) {
MOV(dest.Selem()[1], scratch1.Selem()[newY]);
}
if (newZ != 2) {
MOV(dest.Selem()[2], scratch1.Selem()[newZ]);
}
if (newW != 3) {
MOV(dest.Selem()[3], scratch1.Selem()[newW]);
}
break;
}
}
// Negate the register if necessary
if (negate) {
FNEG(dest.S4(), dest.S4());
}
return; // Return. Rest of the function handles indexing which is not used if index == 0
}
case 1: {
const uintptr_t addrXOffset = uintptr_t(&shader.addrRegister[0]) - uintptr_t(&shader);
LDRSW(X0, statePointer, addrXOffset); // X0 = address register X
break;
}
case 2: {
const uintptr_t addrYOffset = uintptr_t(&shader.addrRegister[1]) - uintptr_t(&shader);
LDRSW(X0, statePointer, addrYOffset); // X0 = address register Y
break;
}
case 3: {
const uintptr_t loopCounterOffset = uintptr_t(&shader.loopCounter) - uintptr_t(&shader);
LDR(W0, statePointer, loopCounterOffset); // X0 = loop counter
break;
}
default: Helpers::panic("[ShaderJIT]: Unimplemented source index type %d", index);
}
// Swizzle and load register into dest, from [state pointer + X1 + offset] and apply the relevant swizzle. Thrashes X2
auto swizzleAndLoadReg = [this, &dest, &compSwizzle](size_t offset) {
MOV(X2, offset);
ADD(X1, X1, X2);
LDR(dest, statePointer, X1);
switch (compSwizzle) {
case noSwizzle: break; // .xyzw
case 0x0: DUP(dest.S4(), dest.Selem()[0]); break; // .xxxx
case 0x55: DUP(dest.S4(), dest.Selem()[1]); break; // .yyyy
case 0xAA: DUP(dest.S4(), dest.Selem()[2]); break; // .zzzz
case 0xFF:
DUP(dest.S4(), dest.Selem()[3]);
break; // .wwww
// Some of these cases may still be optimizable
default: {
MOV(scratch1.B16(), dest.B16()); // Make a copy of the register
const auto newX = getBits<6, 2>(compSwizzle);
const auto newY = getBits<4, 2>(compSwizzle);
const auto newZ = getBits<2, 2>(compSwizzle);
const auto newW = getBits<0, 2>(compSwizzle);
// If the lane swizzled into the new x component is NOT the current x component, swizzle the correct lane with a mov
// Repeat for each component of the vector
if (newX != 0) {
MOV(dest.Selem()[0], scratch1.Selem()[newX]);
}
if (newY != 1) {
MOV(dest.Selem()[1], scratch1.Selem()[newY]);
}
if (newZ != 2) {
MOV(dest.Selem()[2], scratch1.Selem()[newZ]);
}
if (newW != 3) {
MOV(dest.Selem()[3], scratch1.Selem()[newW]);
}
break;
}
}
};
// Here we handle what happens when using indexed addressing & we can't predict what register will be read at compile time
// The index of the access is assumed to be in X0
// Add source register (src) and index (X0) to form the final register
ADD(X0, X0, src);
Label maybeTemp, maybeUniform, unknownReg, end;
const uintptr_t inputOffset = uintptr_t(&shader.inputs[0]) - uintptr_t(&shader);
const uintptr_t tempOffset = uintptr_t(&shader.tempRegisters[0]) - uintptr_t(&shader);
const uintptr_t uniformOffset = uintptr_t(&shader.floatUniforms[0]) - uintptr_t(&shader);
// If reg < 0x10, return inputRegisters[reg]
CMP(X0, 0x10);
B(HS, maybeTemp);
LSL(X1, X0, 4);
swizzleAndLoadReg(inputOffset);
B(end);
// If (reg < 0x1F) return tempRegisters[reg - 0x10]
l(maybeTemp);
CMP(X0, 0x20);
B(HS, maybeUniform);
SUB(X1, X0, 0x10);
LSL(X1, X1, 4);
swizzleAndLoadReg(tempOffset);
B(end);
// If (reg < 0x80) return floatUniforms[reg - 0x20]
l(maybeUniform);
CMP(X0, 0x80);
B(HS, unknownReg);
SUB(X1, X0, 0x20);
LSL(X1, X1, 4);
swizzleAndLoadReg(uniformOffset);
B(end);
l(unknownReg);
MOVI(dest.S4(), 0); // Set dest to 0 if we're reading from a garbage register
l(end);
// Negate the register if necessary
if (negate) {
FNEG(dest.S4(), dest.S4());
}
}
void ShaderEmitter::storeRegister(QReg source, const PICAShader& shader, u32 dest, u32 operandDescriptor) {
const vec4f& destRef = getDestRef(shader, dest);
const uintptr_t offset = uintptr_t(&destRef) - uintptr_t(&shader); // Calculate offset of register from start of the state struct
// Mask of which lanes to write
u32 writeMask = operandDescriptor & 0xf;
if (writeMask == 0xf) { // No lanes are masked, just use STR
STR(source, statePointer, offset);
} else {
LDR(scratch1, statePointer, offset); // Load current value
LDR(scratch2, blendMasks.ptr<u8*>() + writeMask * 16); // Load write mask for blending
BSL(scratch2.B16(), source.B16(), scratch1.B16()); // Scratch2 = (Source & mask) | (original & ~mask)
STR(scratch2, statePointer, offset); // Write it back
}
}
void ShaderEmitter::recMOV(const PICAShader& shader, u32 instruction) {
const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f];
const u32 src = getBits<12, 7>(instruction);
const u32 idx = getBits<19, 2>(instruction);
const u32 dest = getBits<21, 5>(instruction);
loadRegister<1>(src1_vec, shader, src, idx, operandDescriptor); // Load source 1 into scratch1
storeRegister(src1_vec, shader, dest, operandDescriptor);
}
void ShaderEmitter::recFLR(const PICAShader& shader, u32 instruction) {
const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f];
const u32 src = getBits<12, 7>(instruction);
const u32 idx = getBits<19, 2>(instruction);
const u32 dest = getBits<21, 5>(instruction);
loadRegister<1>(src1_vec, shader, src, idx, operandDescriptor); // Load source 1 into scratch1
FRINTM(src1_vec.S4(), src1_vec.S4()); // Floor it and store into dest
storeRegister(src1_vec, shader, dest, operandDescriptor);
}
void ShaderEmitter::recMOVA(const PICAShader& shader, u32 instruction) {
const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f];
const u32 src = getBits<12, 7>(instruction);
const u32 idx = getBits<19, 2>(instruction);
const bool writeX = getBit<3>(operandDescriptor); // Should we write the x component of the address register?
const bool writeY = getBit<2>(operandDescriptor);
static_assert(sizeof(shader.addrRegister) == 2 * sizeof(s32)); // Assert that the address register is 2 s32s
const uintptr_t addrRegisterOffset = uintptr_t(&shader.addrRegister[0]) - uintptr_t(&shader);
const uintptr_t addrRegisterYOffset = addrRegisterOffset + sizeof(shader.addrRegister[0]);
// If no register is being written to then it is a nop. Probably not common but whatever
if (!writeX && !writeY) return;
loadRegister<1>(src1_vec, shader, src, idx, operandDescriptor);
FCVTZS(src1_vec.S4(), src1_vec.S4()); // Convert src1 from floats to s32s with truncation
// Write both together
if (writeX && writeY) {
STR(src1_vec.toD(), statePointer, addrRegisterOffset);
} else if (writeX) {
STR(src1_vec.toS(), statePointer, addrRegisterOffset);
} else if (writeY) {
MOV(W0, src1_vec.Selem()[1]); // W0 = Y component
STR(W0, statePointer, addrRegisterYOffset);
}
}
void ShaderEmitter::recDP3(const PICAShader& shader, u32 instruction) {
const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f];
const u32 src1 = getBits<12, 7>(instruction);
const u32 src2 = getBits<7, 5>(instruction);
const u32 idx = getBits<19, 2>(instruction);
const u32 dest = getBits<21, 5>(instruction);
const u32 writeMask = getBits<0, 4>(operandDescriptor);
// TODO: Safe multiplication equivalent (Multiplication is not IEEE compliant on the PICA)
loadRegister<1>(src1_vec, shader, src1, idx, operandDescriptor);
loadRegister<2>(src2_vec, shader, src2, 0, operandDescriptor);
// Set W component of src1 to 0.0, so that the w factor of the following dp4 will become 0, making it equivalent to a dp3
INS(src1_vec.Selem()[3], WZR);
// Now do a full DP4
FMUL(src1_vec.S4(), src1_vec.S4(), src2_vec.S4()); // Do a piecewise multiplication of the vectors first
FADDP(src1_vec.S4(), src1_vec.S4(), src1_vec.S4()); // Now add the adjacent components together
FADDP(src1_vec.toS(), src1_vec.toD().S2()); // Again for the bottom 2 lanes. Now the bottom lane contains the dot product
if (writeMask != 0x8) { // Copy bottom lane to all lanes if we're not simply writing back x
DUP(src1_vec.S4(), src1_vec.Selem()[0]); // src1_vec = src1_vec.xxxx
}
storeRegister(src1_vec, shader, dest, operandDescriptor);
}
void ShaderEmitter::recDP4(const PICAShader& shader, u32 instruction) {
const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f];
const u32 src1 = getBits<12, 7>(instruction);
const u32 src2 = getBits<7, 5>(instruction);
const u32 idx = getBits<19, 2>(instruction);
const u32 dest = getBits<21, 5>(instruction);
const u32 writeMask = getBits<0, 4>(operandDescriptor);
// TODO: Safe multiplication equivalent (Multiplication is not IEEE compliant on the PICA)
loadRegister<1>(src1_vec, shader, src1, idx, operandDescriptor);
loadRegister<2>(src2_vec, shader, src2, 0, operandDescriptor);
FMUL(src1_vec.S4(), src1_vec.S4(), src2_vec.S4()); // Do a piecewise multiplication of the vectors first
FADDP(src1_vec.S4(), src1_vec.S4(), src1_vec.S4()); // Now add the adjacent components together
FADDP(src1_vec.toS(), src1_vec.toD().S2()); // Again for the bottom 2 lanes. Now the bottom lane contains the dot product
if (writeMask != 0x8) { // Copy bottom lane to all lanes if we're not simply writing back x
DUP(src1_vec.S4(), src1_vec.Selem()[0]); // src1_vec = src1_vec.xxxx
}
storeRegister(src1_vec, shader, dest, operandDescriptor);
}
void ShaderEmitter::recADD(const PICAShader& shader, u32 instruction) {
const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f];
const u32 src1 = getBits<12, 7>(instruction);
const u32 src2 = getBits<7, 5>(instruction);
const u32 idx = getBits<19, 2>(instruction);
const u32 dest = getBits<21, 5>(instruction);
loadRegister<1>(src1_vec, shader, src1, idx, operandDescriptor);
loadRegister<2>(src2_vec, shader, src2, 0, operandDescriptor);
FADD(src1_vec.S4(), src1_vec.S4(), src2_vec.S4());
storeRegister(src1_vec, shader, dest, operandDescriptor);
}
void ShaderEmitter::recMAX(const PICAShader& shader, u32 instruction) {
const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f];
const u32 src1 = getBits<12, 7>(instruction);
const u32 src2 = getBits<7, 5>(instruction);
const u32 idx = getBits<19, 2>(instruction);
const u32 dest = getBits<21, 5>(instruction);
loadRegister<1>(src1_vec, shader, src1, idx, operandDescriptor);
loadRegister<2>(src2_vec, shader, src2, 0, operandDescriptor);
FMAX(src1_vec.S4(), src1_vec.S4(), src2_vec.S4());
storeRegister(src1_vec, shader, dest, operandDescriptor);
}
void ShaderEmitter::recMIN(const PICAShader& shader, u32 instruction) {
const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f];
const u32 src1 = getBits<12, 7>(instruction);
const u32 src2 = getBits<7, 5>(instruction);
const u32 idx = getBits<19, 2>(instruction);
const u32 dest = getBits<21, 5>(instruction);
loadRegister<1>(src1_vec, shader, src1, idx, operandDescriptor);
loadRegister<2>(src2_vec, shader, src2, 0, operandDescriptor);
FMIN(src1_vec.S4(), src1_vec.S4(), src2_vec.S4());
storeRegister(src1_vec, shader, dest, operandDescriptor);
}
void ShaderEmitter::recMUL(const PICAShader& shader, u32 instruction) {
const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f];
const u32 src1 = getBits<12, 7>(instruction);
const u32 src2 = getBits<7, 5>(instruction); // src2 coming first because PICA moment
const u32 idx = getBits<19, 2>(instruction);
const u32 dest = getBits<21, 5>(instruction);
// TODO: Safe multiplication equivalent (Multiplication is not IEEE compliant on the PICA)
loadRegister<1>(src1_vec, shader, src1, idx, operandDescriptor);
loadRegister<2>(src2_vec, shader, src2, 0, operandDescriptor);
FMUL(src1_vec.S4(), src1_vec.S4(), src2_vec.S4());
storeRegister(src1_vec, shader, dest, operandDescriptor);
}
void ShaderEmitter::recRCP(const PICAShader& shader, u32 instruction) {
const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f];
const u32 src = getBits<12, 7>(instruction);
const u32 idx = getBits<19, 2>(instruction);
const u32 dest = getBits<21, 5>(instruction);
const u32 writeMask = operandDescriptor & 0xf;
loadRegister<1>(src1_vec, shader, src, idx, operandDescriptor); // Load source 1 into scratch1
FDIV(src1_vec.toS(), onesVector.toS(), src1_vec.toS()); // src1 = 1.0 / src1
// If we only write back the x component to the result, we needn't perform a shuffle to do res = res.xxxx
// Otherwise we do
if (writeMask != 0x8) { // Copy bottom lane to all lanes if we're not simply writing back x
DUP(src1_vec.S4(), src1_vec.Selem()[0]); // src1_vec = src1_vec.xxxx
}
storeRegister(src1_vec, shader, dest, operandDescriptor);
}
void ShaderEmitter::recRSQ(const PICAShader& shader, u32 instruction) {
const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f];
const u32 src = getBits<12, 7>(instruction);
const u32 idx = getBits<19, 2>(instruction);
const u32 dest = getBits<21, 5>(instruction);
const u32 writeMask = operandDescriptor & 0xf;
constexpr bool useAccurateRSQ = true;
loadRegister<1>(src1_vec, shader, src, idx, operandDescriptor); // Load source 1 into scratch1
// Compute reciprocal square root approximation
// TODO: Should this use frsqte or fsqrt+div? The former is faster but less accurate
// PICA RSQ uses f24 precision though, so it'll be inherently innacurate, and it's likely using an inaccurate approximation too, seeing as
// It doesn't have regular sqrt/div instructions.
// For now, we default to accurate inverse square root
if constexpr (useAccurateRSQ) {
FSQRT(src1_vec.toS(), src1_vec.toS()); // src1 = sqrt(src1), scalar
FDIV(src1_vec.toS(), onesVector.toS(), src1_vec.toS()); // Now invert src1
} else {
FRSQRTE(src1_vec.toS(), src1_vec.toS()); // Much nicer
}
// If we only write back the x component to the result, we needn't perform a shuffle to do res = res.xxxx
// Otherwise we do
if (writeMask != 0x8) { // Copy bottom lane to all lanes if we're not simply writing back x
DUP(src1_vec.S4(), src1_vec.Selem()[0]); // src1_vec = src1_vec.xxxx
}
storeRegister(src1_vec, shader, dest, operandDescriptor);
}
void ShaderEmitter::recMAD(const PICAShader& shader, u32 instruction) {
const bool isMADI = getBit<29>(instruction) == 0;
const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x1f];
const u32 src1 = getBits<17, 5>(instruction);
const u32 src2 = isMADI ? getBits<12, 5>(instruction) : getBits<10, 7>(instruction);
const u32 src3 = isMADI ? getBits<5, 7>(instruction) : getBits<5, 5>(instruction);
const u32 idx = getBits<22, 2>(instruction);
const u32 dest = getBits<24, 5>(instruction);
loadRegister<1>(src1_vec, shader, src1, 0, operandDescriptor);
loadRegister<2>(src2_vec, shader, src2, isMADI ? 0 : idx, operandDescriptor);
loadRegister<3>(src3_vec, shader, src3, isMADI ? idx : 0, operandDescriptor);
// TODO: Safe PICA multiplication
FMLA(src3_vec.S4(), src1_vec.S4(), src2_vec.S4());
storeRegister(src3_vec, shader, dest, operandDescriptor);
}
void ShaderEmitter::recSLT(const PICAShader& shader, u32 instruction) {
const bool isSLTI = (instruction >> 26) == ShaderOpcodes::SLTI;
const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f];
const u32 src1 = isSLTI ? getBits<14, 5>(instruction) : getBits<12, 7>(instruction);
const u32 src2 = isSLTI ? getBits<7, 7>(instruction) : getBits<7, 5>(instruction);
const u32 idx = getBits<19, 2>(instruction);
const u32 dest = getBits<21, 5>(instruction);
loadRegister<1>(src1_vec, shader, src1, isSLTI ? 0 : idx, operandDescriptor);
loadRegister<2>(src2_vec, shader, src2, isSLTI ? idx : 0, operandDescriptor);
// Set each lane of SRC1 to FFFFFFFF if src2 > src1, else to 0. NEON does not have FCMLT so we use FCMGT with inverted operands
// This is more or less a direct port of the relevant x64 JIT code
FCMGT(src1_vec.S4(), src2_vec.S4(), src1_vec.S4());
AND(src1_vec.B16(), src1_vec.B16(), onesVector.B16()); // AND with vec4(1.0) to convert the FFFFFFFF lanes into 1.0
storeRegister(src1_vec, shader, dest, operandDescriptor);
}
void ShaderEmitter::recSGE(const PICAShader& shader, u32 instruction) {
const bool isSGEI = (instruction >> 26) == ShaderOpcodes::SGEI;
const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f];
const u32 src1 = isSGEI ? getBits<14, 5>(instruction) : getBits<12, 7>(instruction);
const u32 src2 = isSGEI ? getBits<7, 7>(instruction) : getBits<7, 5>(instruction);
const u32 idx = getBits<19, 2>(instruction);
const u32 dest = getBits<21, 5>(instruction);
loadRegister<1>(src1_vec, shader, src1, isSGEI ? 0 : idx, operandDescriptor);
loadRegister<2>(src2_vec, shader, src2, isSGEI ? idx : 0, operandDescriptor);
// Set each lane of SRC1 to FFFFFFFF if src1 >= src2, else to 0.
// This is more or less a direct port of the relevant x64 JIT code
FCMGE(src1_vec.S4(), src1_vec.S4(), src2_vec.S4());
AND(src1_vec.B16(), src1_vec.B16(), onesVector.B16()); // AND with vec4(1.0) to convert the FFFFFFFF lanes into 1.0
storeRegister(src1_vec, shader, dest, operandDescriptor);
}
void ShaderEmitter::recCMP(const PICAShader& shader, u32 instruction) {
const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f];
const u32 src1 = getBits<12, 7>(instruction);
const u32 src2 = getBits<7, 5>(instruction); // src2 coming first because PICA moment
const u32 idx = getBits<19, 2>(instruction);
const u32 cmpY = getBits<21, 3>(instruction);
const u32 cmpX = getBits<24, 3>(instruction);
loadRegister<1>(src1_vec, shader, src1, idx, operandDescriptor);
loadRegister<2>(src2_vec, shader, src2, 0, operandDescriptor);
// Map from PICA condition codes (used as index) to x86 condition codes
// We treat invalid condition codes as "always" as suggested by 3DBrew
static constexpr std::array<oaknut::Cond, 8> conditionCodes = {
oaknut::util::EQ, oaknut::util::NE, oaknut::util::LT, oaknut::util::LE,
oaknut::util::GT, oaknut::util::GE, oaknut::util::AL, oaknut::util::AL,
};
static_assert(sizeof(shader.cmpRegister[0]) == 1 && sizeof(shader.cmpRegister) == 2); // The code below relies on bool being 1 byte exactly
const size_t cmpRegXOffset = uintptr_t(&shader.cmpRegister[0]) - uintptr_t(&shader);
// NEON doesn't have SIMD comparisons to do fun stuff with like on x64
FCMP(src1_vec.toS(), src2_vec.toS());
CSET(W0, conditionCodes[cmpX]);
// Compare Y components, which annoyingly enough can't be done without moving
MOV(scratch1.toS(), src1_vec.Selem()[1]);
MOV(scratch2.toS(), src2_vec.Selem()[1]);
FCMP(scratch1.toS(), scratch2.toS());
CSET(W1, conditionCodes[cmpY]);
// Merge the booleans and write them back in one STRh
ORR(W0, W0, W1, LogShift::LSL, 8);
STRH(W0, statePointer, cmpRegXOffset);
}
void ShaderEmitter::checkBoolUniform(const PICAShader& shader, u32 instruction) {
const u32 bit = getBits<22, 4>(instruction); // Bit of the bool uniform to check
const uintptr_t boolUniformOffset = uintptr_t(&shader.boolUniform) - uintptr_t(&shader);
LDRH(W0, statePointer, boolUniformOffset); // Load bool uniform into w0
TST(W0, 1 << bit); // Check if bit is set
}
void ShaderEmitter::checkCmpRegister(const PICAShader& shader, u32 instruction) {
static_assert(sizeof(bool) == 1 && sizeof(shader.cmpRegister) == 2); // The code below relies on bool being 1 byte exactly
const size_t cmpRegXOffset = uintptr_t(&shader.cmpRegister[0]) - uintptr_t(&shader);
const size_t cmpRegYOffset = cmpRegXOffset + sizeof(bool);
const u32 condition = getBits<22, 2>(instruction);
const uint refY = getBit<24>(instruction);
const uint refX = getBit<25>(instruction);
// refX in the bottom byte, refY in the top byte. This is done for condition codes 0 and 1 which check both x and y, so we can emit a single
// instruction that checks both
const u16 refX_refY_merged = refX | (refY << 8);
switch (condition) {
case 0: // Either cmp register matches
LDRB(W0, statePointer, cmpRegXOffset);
LDRB(W1, statePointer, cmpRegYOffset);
// Check if x matches refX
CMP(W0, refX);
CSET(W0, EQ);
// Check if y matches refY
CMP(W1, refY);
CSET(W1, EQ);
// Set Z to 1 if at least one of them matches
ORR(W0, W0, W1);
CMP(W0, 1);
break;
case 1: // Both cmp registers match
LDRH(W0, statePointer, cmpRegXOffset);
// If ref fits in 8 bits, use a single CMP, otherwise move into register and then CMP
if (refX_refY_merged <= 0xff) {
CMP(W0, refX_refY_merged);
} else {
MOV(W1, refX_refY_merged);
CMP(W0, W1);
}
break;
case 2: // At least cmp.x matches
LDRB(W0, statePointer, cmpRegXOffset);
CMP(W0, refX);
break;
default: // At least cmp.y matches
LDRB(W0, statePointer, cmpRegYOffset);
CMP(W0, refY);
break;
}
}
void ShaderEmitter::recCALL(const PICAShader& shader, u32 instruction) {
const u32 num = instruction & 0xff;
const u32 dest = getBits<10, 12>(instruction);
// Push return PC as stack parameter. This is a decently fast solution and Citra does the same but we should probably switch to a proper PICA-like
// Callstack, because it's not great to have an infinitely expanding call stack
MOV(X0, dest + num);
// Push return PC + current link register so that we'll be able to return later
STP(X0, X30, SP, PRE_INDEXED, -16);
// Call subroutine, Oaknut will update the label if it hasn't been initialized yet
BL(instructionLabels[dest]);
// Fetch original LR and return. This also restores SP to its original value, discarding the return guard into XZR
LDP(XZR, X30, SP, POST_INDEXED, 16);
}
void ShaderEmitter::recCALLC(const PICAShader& shader, u32 instruction) {
Label skipCall;
// z is 1 if the call should be taken, 0 otherwise
checkCmpRegister(shader, instruction);
B(NE, skipCall);
recCALL(shader, instruction);
l(skipCall);
}
void ShaderEmitter::recCALLU(const PICAShader& shader, u32 instruction) {
Label skipCall;
// z is 0 if the call should be taken, 1 otherwise
checkBoolUniform(shader, instruction);
B(EQ, skipCall);
recCALL(shader, instruction);
l(skipCall);
}
void ShaderEmitter::recIFC(const PICAShader& shader, u32 instruction) {
// z is 1 if true, else 0
checkCmpRegister(shader, instruction);
const u32 num = instruction & 0xff;
const u32 dest = getBits<10, 12>(instruction);
if (dest < recompilerPC) {
Helpers::warn("Shader JIT: IFC instruction with dest < current PC\n");
}
Label elseBlock, endIf;
// Jump to else block if z is 0
B(NE, elseBlock);
compileUntil(shader, dest);
if (num == 0) { // Else block is empty,
l(elseBlock);
} else { // Else block is NOT empty
B(endIf); // Skip executing the else branch if the if branch was ran
l(elseBlock);
compileUntil(shader, dest + num);
l(endIf);
}
}
void ShaderEmitter::recIFU(const PICAShader& shader, u32 instruction) {
// z is 0 if true, else 1
checkBoolUniform(shader, instruction);
const u32 num = instruction & 0xff;
const u32 dest = getBits<10, 12>(instruction);
if (dest < recompilerPC) {
Helpers::warn("Shader JIT: IFC instruction with dest < current PC\n");
}
Label elseBlock, endIf;
// Jump to else block if z is 1
B(EQ, elseBlock);
compileUntil(shader, dest);
if (num == 0) { // Else block is empty,
l(elseBlock);
} else { // Else block is NOT empty
B(endIf); // Skip executing the else branch if the if branch was ran
l(elseBlock);
compileUntil(shader, dest + num);
l(endIf);
}
}
void ShaderEmitter::recJMPC(const PICAShader& shader, u32 instruction) {
const u32 dest = getBits<10, 12>(instruction);
Label& l = instructionLabels[dest];
// Z is 1 if the comparison is true
checkCmpRegister(shader, instruction);
B(EQ, l);
}
void ShaderEmitter::recJMPU(const PICAShader& shader, u32 instruction) {
bool jumpIfFalse = instruction & 1; // If the LSB is 0 we want to compare to true, otherwise compare to false
const u32 dest = getBits<10, 12>(instruction);
Label& l = instructionLabels[dest];
// Z is 0 if the uniform is true
checkBoolUniform(shader, instruction);
if (jumpIfFalse) {
B(EQ, l);
} else {
B(NE, l);
}
}
void ShaderEmitter::recLOOP(const PICAShader& shader, u32 instruction) {
const u32 dest = getBits<10, 12>(instruction);
const u32 uniformIndex = getBits<22, 2>(instruction);
if (loopLevel > 0) {
log("[Shader JIT] Detected nested loop. Might be broken?\n");
}
if (dest < recompilerPC) {
Helpers::panic("[Shader JIT] Detected backwards loop\n");
}
loopLevel++;
// Offset of the uniform
const auto& uniform = shader.intUniforms[uniformIndex];
const uintptr_t uniformOffset = uintptr_t(&uniform[0]) - uintptr_t(&shader);
// Offset of the loop register
const uintptr_t loopRegOffset = uintptr_t(&shader.loopCounter) - uintptr_t(&shader);
LDRB(W0, statePointer, uniformOffset); // W0 = loop iteration count
LDRB(W1, statePointer, uniformOffset + sizeof(u8)); // W1 = initial loop counter value
LDRB(W2, statePointer, uniformOffset + 2 * sizeof(u8)); // W2 = Loop increment
ADD(W0, W0, 1); // The iteration count is actually uniform.x + 1
STR(W1, statePointer, loopRegOffset); // Set loop counter
// Push loop iteration counter & loop increment
// TODO: This might break if an instruction in a loop decides to yield...
STP(X0, X2, SP, PRE_INDEXED, -16);
Label loopStart, loopEnd;
l(loopStart);
compileUntil(shader, dest + 1);
const size_t stackOffsetOfLoopIncrement = 0;
const size_t stackOffsetOfIterationCounter = stackOffsetOfLoopIncrement + 8;
LDP(X0, X2, SP); // W0 = loop iteration, W2 = loop increment
LDR(W1, statePointer, loopRegOffset); // W1 = loop register
// Increment loop counter
ADD(W1, W1, W2);
STR(W1, statePointer, loopRegOffset);
// Subtract 1 from loop iteration counter,
SUBS(W0, W0, 1);
B(EQ, loopEnd);
// Loop hasn't ended: Write back new iteration counter and go back to the start
STR(X0, SP);
B(loopStart);
l(loopEnd);
// Remove the stuff we pushed on the stack earlier
ADD(SP, SP, 16);
loopLevel--;
}
void ShaderEmitter::recEND(const PICAShader& shader, u32 instruction) {
// Fetch original LR and return. This also restores SP to its original value, discarding the return guard into XZR
LDP(XZR, X30, SP, POST_INDEXED, 16);
RET();
}
#endif

View file

@ -235,6 +235,8 @@ void ShaderEmitter::loadRegister(Xmm dest, const PICAShader& shader, u32 src, u3
compSwizzle = getBits<23, 8>(operandDescriptor);
}
// TODO: Do indexes get applied if src < 0x20?
// PICA has the swizzle descriptor inverted in comparison to x86. For the PICA, the descriptor is (lowest to highest bits) wzyx while it's xyzw for x86
u32 convertedSwizzle = ((compSwizzle >> 6) & 0b11) | (((compSwizzle >> 4) & 0b11) << 2) | (((compSwizzle >> 2) & 0b11) << 4) | ((compSwizzle & 0b11) << 6);
@ -342,10 +344,10 @@ void ShaderEmitter::storeRegister(Xmm source, const PICAShader& shader, u32 dest
} else if (std::popcount(writeMask) == 1) { // Only 1 register needs to be written back. This can be done with a simple shift right + movss
int bit = std::countr_zero(writeMask); // Get which PICA register needs to be written to (0 = w, 1 = z, etc)
size_t index = 3 - bit;
const uintptr_t lane_offset = offset + index * sizeof(float);
const uintptr_t laneOffset = offset + index * sizeof(float);
if (index == 0) { // Bottom lane, no need to shift
movss(dword[statePointer + lane_offset], source);
movss(dword[statePointer + laneOffset], source);
} else { // Shift right by 32 * index, then write bottom lane
if (haveAVX) {
vpsrldq(scratch1, source, index * sizeof(float));
@ -353,7 +355,7 @@ void ShaderEmitter::storeRegister(Xmm source, const PICAShader& shader, u32 dest
movaps(scratch1, source);
psrldq(scratch1, index * sizeof(float));
}
movss(dword[statePointer + lane_offset], scratch1);
movss(dword[statePointer + laneOffset], scratch1);
}
} else if (haveSSE4_1) {
// Bit reverse the write mask because that is what blendps expects
@ -403,11 +405,18 @@ void ShaderEmitter::checkCmpRegister(const PICAShader& shader, u32 instruction)
switch (condition) {
case 0: // Either cmp register matches
// Z flag is 0 if at least 1 of them is set
test(word[statePointer + cmpRegXOffset], refX_refY_merged);
// Invert z flag
setz(al);
test(al, al);
// Check if X matches
cmp(byte[statePointer + cmpRegXOffset], refX);
sete(al);
// Or if Y matches
cmp(byte[statePointer + cmpRegYOffset], refY);
sete(cl);
or_(al, cl);
// If either of them matches, set Z to 1, else set it to 0
xor_(al, 1);
break;
case 1: // Both cmp registers match
cmp(word[statePointer + cmpRegXOffset], refX_refY_merged);
@ -838,7 +847,7 @@ void ShaderEmitter::recCALL(const PICAShader& shader, u32 instruction) {
const u32 dest = getBits<10, 12>(instruction);
// Push return PC as stack parameter. This is a decently fast solution and Citra does the same but we should probably switch to a proper PICA-like
// Callstack, because it's not great to have an infinitely expanding call stack where popping from empty stack is undefined as hell
// Callstack, because it's not great to have an infinitely expanding call stack where popping from empty stack is undefined
push(qword, dest + num);
// Call subroutine, Xbyak will update the label if it hasn't been initialized yet
call(instructionLabels[dest]);

View file

@ -139,6 +139,65 @@ void ActionReplay::executeDType(const Cheat& cheat, u32 instruction) {
switch (instruction) {
case 0xD3000000: offset1 = cheat[pc++]; break;
case 0xD3000001: offset2 = cheat[pc++]; break;
case 0xD6000000:
write32(*activeOffset + cheat[pc++], u32(*activeData));
*activeOffset += 4;
break;
case 0xD6000001:
write32(*activeOffset + cheat[pc++], u32(data1));
*activeOffset += 4;
break;
case 0xD6000002:
write32(*activeOffset + cheat[pc++], u32(data2));
*activeOffset += 4;
break;
case 0xD7000000:
write16(*activeOffset + cheat[pc++], u16(*activeData));
*activeOffset += 2;
break;
case 0xD7000001:
write16(*activeOffset + cheat[pc++], u16(data1));
*activeOffset += 2;
break;
case 0xD7000002:
write16(*activeOffset + cheat[pc++], u16(data2));
*activeOffset += 2;
break;
case 0xD8000000:
write8(*activeOffset + cheat[pc++], u8(*activeData));
*activeOffset += 1;
break;
case 0xD8000001:
write8(*activeOffset + cheat[pc++], u8(data1));
*activeOffset += 1;
break;
case 0xD8000002:
write8(*activeOffset + cheat[pc++], u8(data2));
*activeOffset += 1;
break;
case 0xD9000000: *activeData = read32(cheat[pc++] + *activeOffset); break;
case 0xD9000001: data1 = read32(cheat[pc++] + *activeOffset); break;
case 0xD9000002: data2 = read32(cheat[pc++] + *activeOffset); break;
case 0xDA000000: *activeData = read16(cheat[pc++] + *activeOffset); break;
case 0xDA000001: data1 = read16(cheat[pc++] + *activeOffset); break;
case 0xDA000002: data2 = read16(cheat[pc++] + *activeOffset); break;
case 0xDB000000: *activeData = read8(cheat[pc++] + *activeOffset); break;
case 0xDB000001: data1 = read8(cheat[pc++] + *activeOffset); break;
case 0xDB000002: data2 = read8(cheat[pc++] + *activeOffset); break;
case 0xDC000000: *activeOffset += cheat[pc++]; break;
// DD000000 XXXXXXXX - if KEYPAD has value XXXXXXXX execute next block

View file

@ -1,11 +1,17 @@
#include "applets/applet_manager.hpp"
#include "services/apt.hpp"
using namespace Applets;
AppletManager::AppletManager(Memory& mem) : miiSelector(mem), swkbd(mem) {}
AppletManager::AppletManager(Memory& mem) : miiSelector(mem, nextParameter), swkbd(mem, nextParameter), error(mem, nextParameter) {}
void AppletManager::reset() {
nextParameter = std::nullopt;
miiSelector.reset();
swkbd.reset();
error.reset();
}
AppletBase* AppletManager::getApplet(u32 id) {
@ -16,6 +22,40 @@ AppletBase* AppletManager::getApplet(u32 id) {
case AppletIDs::SoftwareKeyboard:
case AppletIDs::SoftwareKeyboard2: return &swkbd;
case AppletIDs::ErrDisp:
case AppletIDs::ErrDisp2: return &error;
default: return nullptr;
}
}
Applets::Parameter AppletManager::glanceParameter() {
if (nextParameter) {
// Copy parameter
Applets::Parameter param = nextParameter.value();
// APT module clears next parameter even for GlanceParameter for these 2 signals
if (param.signal == static_cast<u32>(APTSignal::DspWakeup) || param.signal == static_cast<u32>(APTSignal::DspSleep)) {
nextParameter = std::nullopt;
}
return param;
}
// Default return value. This is legacy code from before applets were implemented. TODO: Update it
else {
return Applets::Parameter{
.senderID = 0,
.destID = Applets::AppletIDs::Application,
.signal = static_cast<u32>(APTSignal::Wakeup),
.data = {},
};
}
}
Applets::Parameter AppletManager::receiveParameter() {
Applets::Parameter param = glanceParameter();
// ReceiveParameter always clears nextParameter whereas glanceParameter does not
nextParameter = std::nullopt;
return param;
}

View file

@ -0,0 +1,32 @@
#include "applets/error_applet.hpp"
#include "kernel/handles.hpp"
using namespace Applets;
void ErrorApplet::reset() {}
Result::HorizonResult ErrorApplet::start(const MemoryBlock* sharedMem, const std::vector<u8>& parameters, u32 appID) {
Applets::Parameter param = Applets::Parameter{
.senderID = appID,
.destID = AppletIDs::Application,
.signal = static_cast<u32>(APTSignal::WakeupByExit),
.object = 0,
.data = parameters, // TODO: Figure out how the data format for this applet
};
nextParameter = param;
return Result::Success;
}
Result::HorizonResult ErrorApplet::receiveParameter(const Applets::Parameter& parameter) {
Applets::Parameter param = Applets::Parameter{
.senderID = parameter.destID,
.destID = AppletIDs::Application,
.signal = static_cast<u32>(APTSignal::Response),
.object = KernelHandles::APTCaptureSharedMemHandle,
.data = {},
};
nextParameter = param;
return Result::Success;
}

View file

@ -1,11 +1,86 @@
#include "applets/mii_selector.hpp"
#include <boost/crc.hpp>
#include <limits>
#include "kernel/handles.hpp"
using namespace Applets;
void MiiSelectorApplet::reset() {}
Result::HorizonResult MiiSelectorApplet::start() { return Result::Success; }
Result::HorizonResult MiiSelectorApplet::start(const MemoryBlock* sharedMem, const std::vector<u8>& parameters, u32 appID) {
// Get mii configuration from the application
std::memcpy(&config, &parameters[0], sizeof(config));
Result::HorizonResult MiiSelectorApplet::receiveParameter() {
Helpers::warn("Mii Selector: Unimplemented ReceiveParameter");
Applets::Parameter param = Applets::Parameter{
.senderID = appID,
.destID = AppletIDs::Application,
.signal = static_cast<u32>(APTSignal::WakeupByExit),
.object = 0,
};
// Thanks to Citra devs as always for the default mii data and other applet help
output = getDefaultMii();
output.returnCode = 0; // Success
output.selectedGuestMiiIndex = std::numeric_limits<u32>::max();
output.miiChecksum = boost::crc<16, 0x1021, 0, 0, false, false>(&output.selectedMiiData, sizeof(MiiData) + sizeof(output.unknown1));
// Copy output into the response parameter
param.data.resize(sizeof(output));
std::memcpy(&param.data[0], &output, sizeof(output));
nextParameter = param;
return Result::Success;
}
}
Result::HorizonResult MiiSelectorApplet::receiveParameter(const Applets::Parameter& parameter) {
Applets::Parameter param = Applets::Parameter{
.senderID = parameter.destID,
.destID = AppletIDs::Application,
.signal = static_cast<u32>(APTSignal::Response),
.object = KernelHandles::APTCaptureSharedMemHandle,
.data = {},
};
nextParameter = param;
return Result::Success;
}
MiiResult MiiSelectorApplet::getDefaultMii() {
// This data was obtained from Citra
MiiData miiData;
miiData.version = 0x03;
miiData.miiOptions = 0x00;
miiData.miiPos = 0x10;
miiData.consoleID = 0x30;
miiData.systemID = 0xD285B6B300C8850A;
miiData.miiID = 0x98391EE4;
miiData.creatorMAC = {0x40, 0xF4, 0x07, 0xB7, 0x37, 0x10};
miiData.padding = 0x0000;
miiData.miiDetails = 0xA600;
miiData.miiName = {'P', 'a', 'n', 'd', 'a', '3', 'D', 'S', 0x0, 0x0};
miiData.height = 0x40;
miiData.width = 0x40;
miiData.faceStyle = 0x00;
miiData.faceDetails = 0x00;
miiData.hairStyle = 0x21;
miiData.hairDetails = 0x01;
miiData.eyeDetails = 0x02684418;
miiData.eyebrowDetails = 0x26344614;
miiData.noseDetails = 0x8112;
miiData.mouthDetails = 0x1768;
miiData.moustacheDetails = 0x0D00;
miiData.beardDetails = 0x0029;
miiData.glassesDetails = 0x0052;
miiData.moleDetails = 0x4850;
miiData.authorName = {u'B', u'O', u'N', u'K', u'E', u'R'};
MiiResult result;
result.returnCode = 0x0;
result.isGuestMiiSelected = 0x0;
result.selectedGuestMiiIndex = std::numeric_limits<u32>::max();
result.selectedMiiData = miiData;
result.guestMiiName.fill(0x0);
return result;
}

View file

@ -1,11 +1,93 @@
#include "applets/software_keyboard.hpp"
#include <cstring>
#include <string>
#include "kernel/handles.hpp"
using namespace Applets;
void SoftwareKeyboardApplet::reset() {}
Result::HorizonResult SoftwareKeyboardApplet::start() { return Result::Success; }
Result::HorizonResult SoftwareKeyboardApplet::receiveParameter() {
Helpers::warn("Software keyboard: Unimplemented ReceiveParameter");
Result::HorizonResult SoftwareKeyboardApplet::receiveParameter(const Applets::Parameter& parameter) {
switch (parameter.signal) {
// Signal == request -> Applet is asking swkbd for a shared memory handle for backing up the framebuffer before opening the applet
case u32(APTSignal::Request): {
Applets::Parameter param = Applets::Parameter{
.senderID = parameter.destID,
.destID = AppletIDs::Application,
.signal = static_cast<u32>(APTSignal::Response),
.object = KernelHandles::APTCaptureSharedMemHandle,
.data = {},
};
nextParameter = param;
break;
}
default: Helpers::panic("Unimplemented swkbd signal %d\n", parameter.signal);
}
return Result::Success;
}
Result::HorizonResult SoftwareKeyboardApplet::start(const MemoryBlock* sharedMem, const std::vector<u8>& parameters, u32 appID) {
if (parameters.size() < sizeof(SoftwareKeyboardConfig)) {
Helpers::warn("SoftwareKeyboard::Start: Invalid size for keyboard configuration");
return Result::Success;
}
if (sharedMem == nullptr) {
Helpers::warn("SoftwareKeyboard: Missing shared memory");
return Result::Success;
}
// Get keyboard configuration from the application
std::memcpy(&config, &parameters[0], sizeof(config));
const std::u16string text = u"Pand";
u32 textAddress = sharedMem->addr;
// Copy text to shared memory the app gave us
for (u32 i = 0; i < text.size(); i++) {
mem.write16(textAddress, u16(text[i]));
textAddress += sizeof(u16);
}
mem.write16(textAddress, 0); // Write UTF-16 null terminator
// Temporarily hardcode the pressed button to be the firs tone
switch (config.numButtonsM1) {
case SoftwareKeyboardButtonConfig::SingleButton: config.returnCode = SoftwareKeyboardResult::D0Click; break;
case SoftwareKeyboardButtonConfig::DualButton: config.returnCode = SoftwareKeyboardResult::D1Click1; break;
case SoftwareKeyboardButtonConfig::TripleButton: config.returnCode = SoftwareKeyboardResult::D2Click2; break;
case SoftwareKeyboardButtonConfig::NoButton: config.returnCode = SoftwareKeyboardResult::None; break;
default: Helpers::warn("Software keyboard: Invalid button mode specification"); break;
}
config.textOffset = 0;
config.textLength = static_cast<u16>(text.size());
static_assert(offsetof(SoftwareKeyboardConfig, textOffset) == 324);
static_assert(offsetof(SoftwareKeyboardConfig, textLength) == 328);
if (config.filterFlags & SoftwareKeyboardFilter::Callback) {
Helpers::warn("Unimplemented software keyboard profanity callback");
}
closeKeyboard(appID);
return Result::Success;
}
void SoftwareKeyboardApplet::closeKeyboard(u32 appID) {
Applets::Parameter param = Applets::Parameter{
.senderID = appID,
.destID = AppletIDs::Application,
.signal = static_cast<u32>(APTSignal::WakeupByExit),
.object = 0,
};
// Copy software keyboard configuration into the response parameter
param.data.resize(sizeof(config));
std::memcpy(&param.data[0], &config, sizeof(config));
nextParameter = param;
}

View file

@ -1,4 +1,5 @@
#include "cheats.hpp"
#include "swap.hpp"
Cheats::Cheats(Memory& mem, HIDService& hid) : ar(mem, hid) { reset(); }
@ -7,9 +8,70 @@ void Cheats::reset() {
ar.reset(); // Reset ActionReplay
}
void Cheats::addCheat(const Cheat& cheat) {
cheats.push_back(cheat);
u32 Cheats::addCheat(const Cheat& cheat) {
cheatsLoaded = true;
// Find an empty slot if a cheat was previously removed
for (size_t i = 0; i < cheats.size(); i++) {
if (cheats[i].type == CheatType::None) {
cheats[i] = cheat;
return i;
}
}
// Otherwise, just add a new slot
cheats.push_back(cheat);
return cheats.size() - 1;
}
u32 Cheats::addCheat(const u8* data, size_t size) {
if ((size % 8) != 0) {
return badCheatHandle;
}
Cheats::Cheat cheat;
cheat.enabled = true;
cheat.type = Cheats::CheatType::ActionReplay;
for (size_t i = 0; i < size; i += 8) {
auto read32 = [](const u8* ptr) { return (u32(ptr[3]) << 24) | (u32(ptr[2]) << 16) | (u32(ptr[1]) << 8) | u32(ptr[0]); };
// Data is passed to us in big endian so we bswap
u32 firstWord = Common::swap32(read32(data + i));
u32 secondWord = Common::swap32(read32(data + i + 4));
cheat.instructions.insert(cheat.instructions.end(), {firstWord, secondWord});
}
return addCheat(cheat);
}
void Cheats::removeCheat(u32 id) {
if (id >= cheats.size()) {
return;
}
// Not using std::erase because we don't want to invalidate cheat IDs
cheats[id].type = CheatType::None;
cheats[id].instructions.clear();
// Check if no cheats are loaded
for (const auto& cheat : cheats) {
if (cheat.type != CheatType::None) return;
}
cheatsLoaded = false;
}
void Cheats::enableCheat(u32 id) {
if (id < cheats.size()) {
cheats[id].enabled = true;
}
}
void Cheats::disableCheat(u32 id) {
if (id < cheats.size()) {
cheats[id].enabled = false;
}
}
void Cheats::clear() {
@ -19,12 +81,15 @@ void Cheats::clear() {
void Cheats::run() {
for (const Cheat& cheat : cheats) {
if (!cheat.enabled) continue;
switch (cheat.type) {
case CheatType::ActionReplay: {
ar.runCheat(cheat.instructions);
break;
}
case CheatType::None: break;
default: Helpers::panic("Unknown cheat device!");
}
}

View file

@ -142,7 +142,7 @@ std::optional<u32> NCCHArchive::readFile(FileSession* file, u64 offset, u32 size
case PathType::RomFS: {
const u64 romFSSize = cxi->romFS.size;
const u64 romFSOffset = cxi->romFS.offset;
if ((offset >> 32) || (offset >= romFSSize) || (offset + size >= romFSSize)) {
if ((offset >> 32) || (offset >= romFSSize) || (offset + size > romFSSize)) {
Helpers::panic("Tried to read from NCCH with too big of an offset");
}
@ -166,4 +166,4 @@ std::optional<u32> NCCHArchive::readFile(FileSession* file, u64 offset, u32 size
}
return u32(bytesRead);
}
}

View file

@ -45,8 +45,16 @@ HorizonResult SDMCArchive::deleteFile(const FSPath& path) {
FileDescriptor SDMCArchive::openFile(const FSPath& path, const FilePerms& perms) {
FilePerms realPerms = perms;
// SD card always has read permission
realPerms.raw |= (1 << 0);
if (isWriteOnly) {
if (perms.read()) {
Helpers::warn("SDMC: Read flag is not allowed in SDMC Write-Only archive");
return FileError;
}
} else {
// Regular SDMC archive always has read permission
realPerms.raw |= (1 << 0);
}
if ((realPerms.create() && !realPerms.write())) {
Helpers::panic("[SDMC] Unsupported flags for OpenFile");
@ -130,6 +138,11 @@ HorizonResult SDMCArchive::createDirectory(const FSPath& path) {
}
Rust::Result<DirectorySession, HorizonResult> SDMCArchive::openDirectory(const FSPath& path) {
if (isWriteOnly) {
Helpers::warn("SDMC: OpenDirectory is not allowed in SDMC Write-Only archive");
return Err(Result::FS::UnexpectedFileOrDir);
}
if (path.type == PathType::UTF16) {
if (!isPathSafe<PathType::UTF16>(path)) {
Helpers::panic("Unsafe path in SaveData::OpenDirectory");

View file

@ -83,7 +83,7 @@ std::optional<u32> SelfNCCHArchive::readFile(FileSession* file, u64 offset, u32
case PathType::RomFS: {
const u64 romFSSize = cxi->romFS.size;
const u64 romFSOffset = cxi->romFS.offset;
if ((offset >> 32) || (offset >= romFSSize) || (offset + size >= romFSSize)) {
if ((offset >> 32) || (offset >= romFSSize) || (offset + size > romFSSize)) {
Helpers::panic("Tried to read from SelfNCCH with too big of an offset");
}
@ -95,7 +95,7 @@ std::optional<u32> SelfNCCHArchive::readFile(FileSession* file, u64 offset, u32
case PathType::ExeFS: {
const u64 exeFSSize = cxi->exeFS.size;
const u64 exeFSOffset = cxi->exeFS.offset;
if ((offset >> 32) || (offset >= exeFSSize) || (offset + size >= exeFSSize)) {
if ((offset >> 32) || (offset >= exeFSSize) || (offset + size > exeFSSize)) {
Helpers::panic("Tried to read from SelfNCCH with too big of an offset");
}
@ -110,7 +110,7 @@ std::optional<u32> SelfNCCHArchive::readFile(FileSession* file, u64 offset, u32
const u64 romFSSize = cxi->romFS.size;
const u64 romFSOffset = cxi->romFS.offset;
if ((offset >> 32) || (offset >= romFSSize) || (offset + size >= romFSSize)) {
if ((offset >> 32) || (offset >= romFSSize) || (offset + size > romFSSize)) {
Helpers::panic("Tried to read from SelfNCCH with too big of an offset");
}
@ -129,7 +129,7 @@ std::optional<u32> SelfNCCHArchive::readFile(FileSession* file, u64 offset, u32
switch (type) {
case PathType::RomFS: {
const u64 romFSSize = hb3dsx->romFSSize;
if ((offset >> 32) || (offset >= romFSSize) || (offset + size >= romFSSize)) {
if ((offset >> 32) || (offset >= romFSSize) || (offset + size > romFSSize)) {
Helpers::panic("Tried to read from SelfNCCH with too big of an offset");
}
break;
@ -150,4 +150,4 @@ std::optional<u32> SelfNCCHArchive::readFile(FileSession* file, u64 offset, u32
}
return u32(bytesRead);
}
}

View file

@ -96,7 +96,7 @@ void Kernel::svcSignalEvent() {
// Result WaitSynchronization1(Handle handle, s64 timeout_nanoseconds)
void Kernel::waitSynchronization1() {
const Handle handle = regs[0];
const s64 ns = s64(u64(regs[1]) | (u64(regs[2]) << 32));
const s64 ns = s64(u64(regs[2]) | (u64(regs[3]) << 32));
logSVC("WaitSynchronization1(handle = %X, ns = %lld)\n", handle, ns);
const auto object = getObject(handle);
@ -126,8 +126,7 @@ void Kernel::waitSynchronization1() {
auto& t = threads[currentThreadIndex];
t.waitList.resize(1);
t.status = ThreadStatus::WaitSync1;
t.sleepTick = cpu.getTicks();
t.waitingNanoseconds = ns;
t.wakeupTick = getWakeupTick(ns);
t.waitList[0] = handle;
// Add the current thread to the object's wait list
@ -220,8 +219,7 @@ void Kernel::waitSynchronizationN() {
t.waitList.resize(handleCount);
t.status = ThreadStatus::WaitSyncAny;
t.outPointer = outPointer;
t.waitingNanoseconds = ns;
t.sleepTick = cpu.getTicks();
t.wakeupTick = getWakeupTick(ns);
for (s32 i = 0; i < handleCount; i++) {
t.waitList[i] = waitObjects[i].first; // Add object to this thread's waitlist

View file

@ -148,6 +148,11 @@ void Kernel::writeFile(u32 messagePointer, Handle fileHandle) {
IOFile f(file->fd);
auto [success, bytesWritten] = f.writeBytes(data.get(), size);
// TODO: Should this check only the byte?
if (writeOption) {
f.flush();
}
mem.write32(messagePointer, IPC::responseHeader(0x0803, 2, 2));
if (!success) {
Helpers::panic("Kernel::WriteFile failed");

View file

@ -8,13 +8,6 @@
The code for our idle thread looks like this
idle_thread_main:
mov r0, #4096 @ Loop counter
.loop:
nop; nop; nop; nop @ NOP 4 times to waste some cycles
subs r0, #1 @ Decrement counter by 1, go back to looping if loop counter != 0
bne .loop
// Sleep for 0 seconds with the SleepThread SVC, which just yields execution
mov r0, #0
mov r1, #0
@ -24,14 +17,10 @@ idle_thread_main:
*/
static constexpr u8 idleThreadCode[] = {
0x01, 0x0A, 0xA0, 0xE3, // mov r0, #4096
0x00, 0xF0, 0x20, 0xE3, 0x00, 0xF0, 0x20, 0xE3, 0x00, 0xF0, 0x20, 0xE3, 0x00, 0xF0, 0x20, 0xE3, // nop (4 times)
0x01, 0x00, 0x50, 0xE2, // subs r0, #1
0xF9, 0xFF, 0xFF, 0x1A, // bne loop
0x00, 0x00, 0xA0, 0xE3, // mov r0, #0
0x00, 0x10, 0xA0, 0xE3, // mov r1, #0
0x0A, 0x00, 0x00, 0xEF, // svc SleepThread
0xF4, 0xFF, 0xFF, 0xEA // b idle_thread_main
0xFB, 0xFF, 0xFF, 0xEA // b idle_thread_main
};
// Set up an idle thread to run when no thread is able to run

View file

@ -50,6 +50,7 @@ void Kernel::serviceSVC(u32 svc) {
case 0x1D: svcClearTimer(); break;
case 0x1E: createMemoryBlock(); break;
case 0x1F: mapMemoryBlock(); break;
case 0x20: unmapMemoryBlock(); break;
case 0x21: createAddressArbiter(); break;
case 0x22: arbitrateAddress(); break;
case 0x23: svcCloseHandle(); break;
@ -66,6 +67,7 @@ void Kernel::serviceSVC(u32 svc) {
case 0x38: getResourceLimit(); break;
case 0x39: getResourceLimitLimitValues(); break;
case 0x3A: getResourceLimitCurrentValues(); break;
case 0x3B: getThreadContext(); break;
case 0x3D: outputDebugString(); break;
default: Helpers::panic("Unimplemented svc: %X @ %08X", svc, regs[15]); break;
}
@ -148,6 +150,7 @@ void Kernel::reset() {
}
objects.clear();
mutexHandles.clear();
timerHandles.clear();
portHandles.clear();
threadIndices.clear();
serviceManager.reset();
@ -178,6 +181,30 @@ u32 Kernel::getTLSPointer() {
// Result CloseHandle(Handle handle)
void Kernel::svcCloseHandle() {
logSVC("CloseHandle(handle = %d) (Unimplemented)\n", regs[0]);
const Handle handle = regs[0];
KernelObject* object = getObject(handle);
if (object != nullptr) {
switch (object->type) {
// Close file descriptor when closing a file to prevent leaks and properly flush file contents
case KernelObjectType::File: {
FileSession* file = object->getData<FileSession>();
if (file->isOpen) {
file->isOpen = false;
if (file->fd != nullptr) {
fclose(file->fd);
file->fd = nullptr;
}
}
break;
}
default: break;
}
}
// Stub to always succeed for now
regs[0] = Result::Success;
}

View file

@ -144,6 +144,7 @@ void Kernel::mapMemoryBlock() {
printf("Mapping CSND memory block\n");
break;
case KernelHandles::APTCaptureSharedMemHandle: break;
default: Helpers::panic("Mapping unknown shared memory block: %X", block);
}
} else {
@ -206,3 +207,12 @@ void Kernel::createMemoryBlock() {
regs[0] = Result::Success;
regs[1] = makeMemoryBlock(addr, size, myPermission, otherPermission);
}
void Kernel::unmapMemoryBlock() {
Handle block = regs[0];
u32 addr = regs[1];
logSVC("Unmap memory block (block handle = %X, addr = %08X)\n", block, addr);
Helpers::warn("Stubbed svcUnmapMemoryBlock!");
regs[0] = Result::Success;
}

View file

@ -52,14 +52,8 @@ bool Kernel::canThreadRun(const Thread& t) {
return true;
} else if (t.status == ThreadStatus::WaitSleep || t.status == ThreadStatus::WaitSync1
|| t.status == ThreadStatus::WaitSyncAny || t.status == ThreadStatus::WaitSyncAll) {
const u64 elapsedTicks = cpu.getTicks() - t.sleepTick;
constexpr double ticksPerSec = double(CPU::ticksPerSec);
constexpr double nsPerTick = ticksPerSec / 1000000000.0;
// TODO: Set r0 to the correct error code on timeout for WaitSync{1/Any/All}
const s64 elapsedNs = s64(double(elapsedTicks) * nsPerTick);
return elapsedNs >= t.waitingNanoseconds;
return cpu.getTicks() >= t.wakeupTick;
}
// Handle timeouts and stuff here
@ -82,6 +76,15 @@ std::optional<int> Kernel::getNextThread() {
return std::nullopt;
}
u64 Kernel::getWakeupTick(s64 ns) {
// Timeout == -1 means that the thread doesn't plan on waking up automatically
if (ns == -1) {
return std::numeric_limits<u64>::max();
}
return cpu.getTicks() + Scheduler::nsToCycles(ns);
}
// See if there is a higher priority, ready thread and switch to that
void Kernel::rescheduleThreads() {
Thread& current = threads[currentThreadIndex]; // Current running thread
@ -368,13 +371,30 @@ void Kernel::sleepThread(s64 ns) {
if (index != idleThreadIndex) {
switchThread(index);
}
} else {
if (currentThreadIndex == idleThreadIndex) {
const Scheduler& scheduler = cpu.getScheduler();
u64 timestamp = scheduler.nextTimestamp;
for (auto i : threadIndices) {
const Thread& t = threads[i];
if (t.status == ThreadStatus::WaitSleep || t.status == ThreadStatus::WaitSync1 || t.status == ThreadStatus::WaitSyncAny ||
t.status == ThreadStatus::WaitSyncAll) {
timestamp = std::min<u64>(timestamp, t.wakeupTick);
}
}
if (timestamp > scheduler.currentTimestamp) {
u64 idleCycles = timestamp - scheduler.currentTimestamp;
cpu.addTicks(idleCycles);
}
}
}
} else { // If we're sleeping for >= 0 ns
Thread& t = threads[currentThreadIndex];
t.status = ThreadStatus::WaitSleep;
t.waitingNanoseconds = ns;
t.sleepTick = cpu.getTicks();
t.wakeupTick = getWakeupTick(ns);
requireReschedule();
}
@ -462,6 +482,13 @@ void Kernel::getThreadIdealProcessor() {
regs[1] = static_cast<u32>(ProcessorID::AppCore);
}
void Kernel::getThreadContext() {
Helpers::warn("Stubbed Kernel::GetThreadContext");
// TODO: Decompile this from Kernel11. 3DBrew says function is stubbed.
regs[0] = Result::Success;
}
void Kernel::setThreadPriority() {
const Handle handle = regs[0];
const u32 priority = regs[1];

View file

@ -1,5 +1,8 @@
#include "kernel.hpp"
#include <limits>
#include "cpu.hpp"
#include "kernel.hpp"
#include "scheduler.hpp"
Handle Kernel::makeTimer(ResetType type) {
Handle ret = makeObject(KernelObjectType::Timer);
@ -9,31 +12,48 @@ Handle Kernel::makeTimer(ResetType type) {
Helpers::panic("Created pulse timer");
}
// timerHandles.push_back(ret);
timerHandles.push_back(ret);
return ret;
}
void Kernel::updateTimer(Handle handle, Timer* timer) {
if (timer->running) {
const u64 currentTicks = cpu.getTicks();
u64 elapsedTicks = currentTicks - timer->startTick;
void Kernel::pollTimers() {
u64 currentTick = cpu.getTicks();
constexpr double ticksPerSec = double(CPU::ticksPerSec);
constexpr double nsPerTick = ticksPerSec / 1000000000.0;
const s64 elapsedNs = s64(double(elapsedTicks) * nsPerTick);
// Find the next timestamp we'll poll KTimers on. To do this, we find the minimum tick one of our timers will fire
u64 nextTimestamp = std::numeric_limits<u64>::max();
// Do we have any active timers anymore? If not, then we won't need to schedule a new timer poll event
bool haveActiveTimers = false;
// Timer has fired
if (elapsedNs >= timer->currentDelay) {
timer->startTick = currentTicks;
timer->currentDelay = timer->interval;
signalTimer(handle, timer);
for (auto handle : timerHandles) {
KernelObject* object = getObject(handle, KernelObjectType::Timer);
if (object != nullptr) {
Timer* timer = object->getData<Timer>();
if (timer->running) {
// If timer has fired, signal it and set the tick it will next time
if (currentTick >= timer->fireTick) {
signalTimer(handle, timer);
}
// Update our next timer fire timestamp and mark that we should schedule a new event to poll timers
// We recheck timer->running because signalling a timer stops it if interval == 0
if (timer->running) {
nextTimestamp = std::min<u64>(nextTimestamp, timer->fireTick);
haveActiveTimers = true;
}
}
}
}
// If we still have active timers, schedule next poll event
if (haveActiveTimers) {
Scheduler& scheduler = cpu.getScheduler();
scheduler.addEvent(Scheduler::EventType::UpdateTimers, nextTimestamp);
}
}
void Kernel::cancelTimer(Timer* timer) {
timer->running = false;
// TODO: When we have a scheduler this should properly cancel timer events in the scheduler
}
void Kernel::signalTimer(Handle timerHandle, Timer* timer) {
@ -51,6 +71,12 @@ void Kernel::signalTimer(Handle timerHandle, Timer* timer) {
case ResetType::Pulse: Helpers::panic("Signalled pulsing timer"); break;
}
}
if (timer->interval == 0) {
cancelTimer(timer);
} else {
timer->fireTick = cpu.getTicks() + Scheduler::nsToCycles(timer->interval);
}
}
void Kernel::svcCreateTimer() {
@ -70,8 +96,8 @@ void Kernel::svcCreateTimer() {
void Kernel::svcSetTimer() {
Handle handle = regs[0];
// TODO: Is this actually s64 or u64? 3DBrew says s64, but u64 makes more sense
const s64 initial = s64(u64(regs[1]) | (u64(regs[2]) << 32));
const s64 interval = s64(u64(regs[3]) | (u64(regs[4]) << 32));
const s64 initial = s64(u64(regs[2]) | (u64(regs[3]) << 32));
const s64 interval = s64(u64(regs[1]) | (u64(regs[4]) << 32));
logSVC("SetTimer (handle = %X, initial delay = %llX, interval delay = %llX)\n", handle, initial, interval);
KernelObject* object = getObject(handle, KernelObjectType::Timer);
@ -83,18 +109,20 @@ void Kernel::svcSetTimer() {
Timer* timer = object->getData<Timer>();
cancelTimer(timer);
timer->currentDelay = initial;
timer->interval = interval;
timer->running = true;
timer->startTick = cpu.getTicks();
timer->fireTick = cpu.getTicks() + Scheduler::nsToCycles(initial);
Scheduler& scheduler = cpu.getScheduler();
// Signal an event to poll timers as soon as possible
scheduler.removeEvent(Scheduler::EventType::UpdateTimers);
scheduler.addEvent(Scheduler::EventType::UpdateTimers, cpu.getTicks() + 1);
// If the initial delay is 0 then instantly signal the timer
if (initial == 0) {
signalTimer(handle, timer);
} else {
// This should schedule an event in the scheduler when we have one
}
regs[0] = Result::Success;
}

View file

@ -26,6 +26,7 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn
codeFile.clear();
saveData.clear();
smdh.clear();
partitionInfo = info;
size = u64(*(u32*)&header[0x104]) * mediaUnit; // TODO: Maybe don't type pun because big endian will break
@ -219,11 +220,10 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn
}
} else if (std::strcmp(name, "icon") == 0) {
// Parse icon file to extract region info and more in the future (logo, etc)
std::vector<u8> tmp;
tmp.resize(fileSize);
readFromFile(file, exeFS, tmp.data(), fileOffset + exeFSHeaderSize, fileSize);
smdh.resize(fileSize);
readFromFile(file, exeFS, smdh.data(), fileOffset + exeFSHeaderSize, fileSize);
if (!parseSMDH(tmp)) {
if (!parseSMDH(smdh)) {
printf("Failed to parse SMDH!\n");
}
}

View file

@ -8,6 +8,7 @@ namespace ACCommands {
CloseAsync = 0x00080004,
GetLastErrorCode = 0x000A0000,
GetStatus = 0x000C0000,
GetWifiStatus = 0x000D0000,
GetConnectingInfraPriority = 0x000F0000,
RegisterDisconnectEvent = 0x00300004,
IsConnected = 0x003E0042,
@ -29,6 +30,7 @@ void ACService::handleSyncRequest(u32 messagePointer) {
case ACCommands::GetConnectingInfraPriority: getConnectingInfraPriority(messagePointer); break;
case ACCommands::GetLastErrorCode: getLastErrorCode(messagePointer); break;
case ACCommands::GetStatus: getStatus(messagePointer); break;
case ACCommands::GetWifiStatus: getWifiStatus(messagePointer); break;
case ACCommands::IsConnected: isConnected(messagePointer); break;
case ACCommands::RegisterDisconnectEvent: registerDisconnectEvent(messagePointer); break;
case ACCommands::SetClientVersion: setClientVersion(messagePointer); break;
@ -91,6 +93,20 @@ void ACService::getStatus(u32 messagePointer) {
mem.write32(messagePointer + 8, 0);
}
void ACService::getWifiStatus(u32 messagePointer) {
log("AC::GetWifiStatus (stubbed)\n");
enum class WifiStatus : u32 {
None = 0,
Slot1 = 1,
Slot2 = 2,
Slot3 = 4,
};
mem.write32(messagePointer, IPC::responseHeader(0x0D, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write32(messagePointer + 8, static_cast<u32>(WifiStatus::None));
}
void ACService::isConnected(u32 messagePointer) {
log("AC::IsConnected\n");

View file

@ -2,6 +2,7 @@
#include "ipc.hpp"
#include "kernel.hpp"
#include <algorithm>
#include <vector>
namespace APTCommands {
@ -32,34 +33,9 @@ namespace APTCommands {
};
}
// https://www.3dbrew.org/wiki/NS_and_APT_Services#Command
namespace APTTransitions {
enum : u32 {
None = 0,
Wakeup = 1,
Request = 2,
Response = 3,
Exit = 4,
Message = 5,
HomeButtonSingle = 6,
HomeButtonDouble = 7,
DSPSleep = 8,
DSPWakeup = 9,
WakeupByExit = 10,
WakuepByPause = 11,
WakeupByCancel = 12,
WakeupByCancelAll = 13,
WakeupByPowerButton = 14,
WakeupToJumpHome = 15,
RequestForApplet = 16,
WakeupToLaunchApp = 17,
ProcessDed = 0x41
};
}
void APTService::reset() {
// Set the default CPU time limit to 30%. Seems safe, as this is what Metroid 2 uses by default
cpuTimeLimit = 30;
// Set the default CPU time limit to 0%. Appears to be the default value on hardware
cpuTimeLimit = 0;
// Reset the handles for the various service objects
lockHandle = std::nullopt;
@ -88,6 +64,7 @@ void APTService::handleSyncRequest(u32 messagePointer) {
case APTCommands::NotifyToWait: notifyToWait(messagePointer); break;
case APTCommands::PreloadLibraryApplet: preloadLibraryApplet(messagePointer); break;
case APTCommands::PrepareToStartLibraryApplet: prepareToStartLibraryApplet(messagePointer); break;
case APTCommands::StartLibraryApplet: startLibraryApplet(messagePointer); break;
case APTCommands::ReceiveParameter: [[likely]] receiveParameter(messagePointer); break;
case APTCommands::ReplySleepQuery: replySleepQuery(messagePointer); break;
case APTCommands::SetApplicationCpuTimeLimit: setApplicationCpuTimeLimit(messagePointer); break;
@ -164,6 +141,39 @@ void APTService::prepareToStartLibraryApplet(u32 messagePointer) {
mem.write32(messagePointer + 4, Result::Success);
}
void APTService::startLibraryApplet(u32 messagePointer) {
const u32 appID = mem.read32(messagePointer + 4);
const u32 bufferSize = mem.read32(messagePointer + 8);
const Handle parameters = mem.read32(messagePointer + 16);
const u32 buffer = mem.read32(messagePointer + 24);
log("APT::StartLibraryApplet (app ID = %X)\n", appID);
Applets::AppletBase* destApplet = appletManager.getApplet(appID);
if (destApplet == nullptr) {
Helpers::warn("APT::StartLibraryApplet: Unimplemented dest applet ID");
mem.write32(messagePointer, IPC::responseHeader(0x1E, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
} else {
KernelObject* sharedMemObject = kernel.getObject(parameters);
const MemoryBlock* sharedMem = sharedMemObject ? sharedMemObject->getData<MemoryBlock>() : nullptr;
std::vector<u8> data;
data.reserve(bufferSize);
for (u32 i = 0; i < bufferSize; i++) {
data.push_back(mem.read8(buffer + i));
}
Result::HorizonResult result = destApplet->start(sharedMem, data, appID);
if (resumeEvent.has_value()) {
kernel.signalEvent(resumeEvent.value());
}
mem.write32(messagePointer, IPC::responseHeader(0x1E, 1, 0));
mem.write32(messagePointer + 4, result);
}
}
void APTService::checkNew3DS(u32 messagePointer) {
log("APT::CheckNew3DS\n");
mem.write32(messagePointer, IPC::responseHeader(0x102, 2, 0));
@ -246,7 +256,7 @@ void APTService::sendParameter(u32 messagePointer) {
const u32 parameterHandle = mem.read32(messagePointer + 24); // What dis?
const u32 parameterPointer = mem.read32(messagePointer + 32);
log("APT::SendParameter (source app = %X, dest app = %X, cmd = %X, size = %X) (Stubbed)", sourceAppID, destAppID, cmd, paramSize);
log("APT::SendParameter (source app = %X, dest app = %X, cmd = %X, size = %X)", sourceAppID, destAppID, cmd, paramSize);
mem.write32(messagePointer, IPC::responseHeader(0x0C, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
@ -259,7 +269,21 @@ void APTService::sendParameter(u32 messagePointer) {
if (destApplet == nullptr) {
Helpers::warn("APT::SendParameter: Unimplemented dest applet ID");
} else {
auto result = destApplet->receiveParameter();
// Construct parameter, send it to applet
Applets::Parameter param;
param.senderID = sourceAppID;
param.destID = destAppID;
param.signal = cmd;
// Fetch parameter data buffer
param.data.reserve(paramSize);
u32 pointer = parameterPointer;
for (u32 i = 0; i < paramSize; i++) {
param.data.push_back(mem.read8(pointer++));
}
auto result = destApplet->receiveParameter(param);
}
if (resumeEvent.has_value()) {
@ -270,37 +294,58 @@ void APTService::sendParameter(u32 messagePointer) {
void APTService::receiveParameter(u32 messagePointer) {
const u32 app = mem.read32(messagePointer + 4);
const u32 size = mem.read32(messagePointer + 8);
log("APT::ReceiveParameter(app ID = %X, size = %04X) (STUBBED)\n", app, size);
// Parameter data pointer is in the thread static buffer, which starts 0x100 bytes after the command buffer
const u32 buffer = mem.read32(messagePointer + 0x100 + 4);
log("APT::ReceiveParameter(app ID = %X, size = %04X)\n", app, size);
if (size > 0x1000) Helpers::panic("APT::ReceiveParameter with size > 0x1000");
auto parameter = appletManager.receiveParameter();
// TODO: Properly implement this. We currently stub somewhat like 3dmoo
mem.write32(messagePointer, IPC::responseHeader(0xD, 4, 4));
mem.write32(messagePointer + 4, Result::Success);
mem.write32(messagePointer + 8, 0); // Sender App ID
mem.write32(messagePointer + 12, APTTransitions::Wakeup); // Command
mem.write32(messagePointer + 16, 0);
// Sender App ID
mem.write32(messagePointer + 8, parameter.senderID);
// Command
mem.write32(messagePointer + 12, parameter.signal);
// Size of parameter data
mem.write32(messagePointer + 16, parameter.data.size());
mem.write32(messagePointer + 20, 0x10);
mem.write32(messagePointer + 24, 0);
mem.write32(messagePointer + 24, parameter.object);
mem.write32(messagePointer + 28, 0);
const u32 transferSize = std::min<u32>(size, parameter.data.size());
for (u32 i = 0; i < transferSize; i++) {
mem.write8(buffer + i, parameter.data[i]);
}
}
void APTService::glanceParameter(u32 messagePointer) {
const u32 app = mem.read32(messagePointer + 4);
const u32 size = mem.read32(messagePointer + 8);
log("APT::GlanceParameter(app ID = %X, size = %04X) (STUBBED)\n", app, size);
// Parameter data pointer is in the thread static buffer, which starts 0x100 bytes after the command buffer
const u32 buffer = mem.read32(messagePointer + 0x100 + 4);
log("APT::GlanceParameter(app ID = %X, size = %04X)\n", app, size);
if (size > 0x1000) Helpers::panic("APT::GlanceParameter with size > 0x1000");
auto parameter = appletManager.glanceParameter();
// TODO: Properly implement this. We currently stub it similar
mem.write32(messagePointer, IPC::responseHeader(0xE, 4, 4));
mem.write32(messagePointer + 4, Result::Success);
mem.write32(messagePointer + 8, 0); // Sender App ID
mem.write32(messagePointer + 12, APTTransitions::Wakeup); // Command
mem.write32(messagePointer + 16, 0);
// Sender App ID
mem.write32(messagePointer + 8, parameter.senderID);
// Command
mem.write32(messagePointer + 12, parameter.signal);
// Size of parameter data
mem.write32(messagePointer + 16, parameter.data.size());
mem.write32(messagePointer + 20, 0);
mem.write32(messagePointer + 24, 0);
mem.write32(messagePointer + 24, parameter.object);
mem.write32(messagePointer + 28, 0);
const u32 transferSize = std::min<u32>(size, parameter.data.size());
for (u32 i = 0; i < transferSize; i++) {
mem.write8(buffer + i, parameter.data[i]);
}
}
void APTService::replySleepQuery(u32 messagePointer) {
@ -314,10 +359,13 @@ void APTService::setApplicationCpuTimeLimit(u32 messagePointer) {
u32 percentage = mem.read32(messagePointer + 8); // CPU time percentage between 5% and 89%
log("APT::SetApplicationCpuTimeLimit (percentage = %d%%)\n", percentage);
mem.write32(messagePointer, IPC::responseHeader(0x4F, 1, 0));
// If called with invalid parameters, the current time limit is left unchanged, and OS::NotImplemented is returned
if (percentage < 5 || percentage > 89 || fixed != 1) {
Helpers::panic("Invalid parameters passed to APT::SetApplicationCpuTimeLimit");
Helpers::warn("Invalid parameter passed to APT::SetApplicationCpuTimeLimit: (percentage, fixed) = (%d, %d)\n", percentage, fixed);
mem.write32(messagePointer + 4, Result::OS::NotImplemented);
} else {
mem.write32(messagePointer, IPC::responseHeader(0x4F, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
cpuTimeLimit = percentage;
}

View file

@ -6,6 +6,7 @@ namespace BOSSCommands {
InitializeSession = 0x00010082,
UnregisterStorage = 0x00030000,
GetTaskStorageInfo = 0x00040000,
GetNewArrivalFlag = 0x00070000,
RegisterNewArrivalEvent = 0x00080002,
SetOptoutFlag = 0x00090040,
GetOptoutFlag = 0x000A0000,
@ -37,6 +38,7 @@ void BOSSService::handleSyncRequest(u32 messagePointer) {
switch (command) {
case BOSSCommands::CancelTask: cancelTask(messagePointer); break;
case BOSSCommands::GetErrorCode: getErrorCode(messagePointer); break;
case BOSSCommands::GetNewArrivalFlag: getNewArrivalFlag(messagePointer); break;
case BOSSCommands::GetNsDataIdList:
case BOSSCommands::GetNsDataIdList1:
getNsDataIdList(messagePointer, command); break;
@ -240,4 +242,11 @@ void BOSSService::unregisterStorage(u32 messagePointer) {
log("BOSS::UnregisterStorage (stubbed)\n");
mem.write32(messagePointer, IPC::responseHeader(0x3, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void BOSSService::getNewArrivalFlag(u32 messagePointer) {
log("BOSS::GetNewArrivalFlag (stubbed)\n");
mem.write32(messagePointer, IPC::responseHeader(0x7, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write8(messagePointer + 8, 0); // Flag
}

View file

@ -1,31 +1,96 @@
#include "services/cam.hpp"
#include <vector>
#include "ipc.hpp"
#include "kernel.hpp"
namespace CAMCommands {
enum : u32 {
StartCapture = 0x00010040,
GetBufferErrorInterruptEvent = 0x00060040,
SetReceiving = 0x00070102,
DriverInitialize = 0x00390000,
DriverFinalize = 0x003A0000,
SetTransferLines = 0x00090100,
GetMaxLines = 0x000A0080,
SetTransferBytes = 0x000B0100,
GetTransferBytes = 0x000C0040,
GetMaxBytes = 0x000D0080,
SetTrimming = 0x000E0080,
SetTrimmingParamsCenter = 0x00120140,
SetSize = 0x001F00C0, // Set size has different headers between cam:u and New3DS QTM module
SetFrameRate = 0x00200080,
SetContrast = 0x00230080,
GetSuitableY2rStandardCoefficient = 0x00360000,
};
}
void CAMService::reset() { bufferErrorInterruptEvents.fill(std::nullopt); }
// Helper struct for working with camera ports
class PortSelect {
u32 value;
public:
PortSelect(u32 val) : value(val) {}
bool isValid() const { return value < 4; }
bool isSinglePort() const {
// 1 corresponds to the first camera port and 2 corresponds to the second port
return value == 1 || value == 2;
}
bool isBothPorts() const {
// 3 corresponds to both ports
return value == 3;
}
// Returns the index of the camera port, assuming that it's only a single port
int getSingleIndex() const {
if (!isSinglePort()) [[unlikely]] {
Helpers::panic("Camera: getSingleIndex called for port with invalid value");
}
return value - 1;
}
std::vector<int> getPortIndices() const {
switch (value) {
case 1: return {0}; // Only port 1
case 2: return {1}; // Only port 2
case 3: return {0, 1}; // Both port 1 and port 2
default: return {}; // No ports or invalid ports
}
}
};
void CAMService::reset() {
for (auto& port : ports) {
port.reset();
}
}
void CAMService::handleSyncRequest(u32 messagePointer) {
const u32 command = mem.read32(messagePointer);
switch (command) {
case CAMCommands::DriverInitialize: driverInitialize(messagePointer); break;
case CAMCommands::DriverFinalize: driverFinalize(messagePointer); break;
case CAMCommands::GetBufferErrorInterruptEvent: getBufferErrorInterruptEvent(messagePointer); break;
case CAMCommands::GetMaxBytes: getMaxBytes(messagePointer); break;
case CAMCommands::GetMaxLines: getMaxLines(messagePointer); break;
case CAMCommands::GetSuitableY2rStandardCoefficient: getSuitableY2RCoefficients(messagePointer); break;
case CAMCommands::GetTransferBytes: getTransferBytes(messagePointer); break;
case CAMCommands::SetContrast: setContrast(messagePointer); break;
case CAMCommands::SetFrameRate: setFrameRate(messagePointer); break;
case CAMCommands::SetReceiving: setReceiving(messagePointer); break;
case CAMCommands::SetSize: setSize(messagePointer); break;
case CAMCommands::SetTransferLines: setTransferLines(messagePointer); break;
case CAMCommands::SetTrimming: setTrimming(messagePointer); break;
case CAMCommands::SetTrimmingParamsCenter: setTrimmingParamsCenter(messagePointer); break;
case CAMCommands::StartCapture: startCapture(messagePointer); break;
default:
Helpers::panic("Unimplemented CAM service requested. Command: %08X\n", command);
Helpers::warn("Unimplemented CAM service requested. Command: %08X\n", command);
mem.write32(messagePointer + 4, Result::Success);
break;
}
}
@ -36,6 +101,12 @@ void CAMService::driverInitialize(u32 messagePointer) {
mem.write32(messagePointer + 4, Result::Success);
}
void CAMService::driverFinalize(u32 messagePointer) {
log("CAM::DriverFinalize\n");
mem.write32(messagePointer, IPC::responseHeader(0x3A, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void CAMService::setContrast(u32 messagePointer) {
const u32 cameraSelect = mem.read32(messagePointer + 4);
const u32 contrast = mem.read32(messagePointer + 8);
@ -46,13 +117,46 @@ void CAMService::setContrast(u32 messagePointer) {
mem.write32(messagePointer + 4, Result::Success);
}
void CAMService::setTransferLines(u32 messagePointer) {
const u32 port = mem.read32(messagePointer + 4);
const s16 lines = mem.read16(messagePointer + 8);
const s16 width = mem.read16(messagePointer + 12);
const s16 height = mem.read16(messagePointer + 16);
void CAMService::setTransferBytes(u32 messagePointer) {
const u32 portIndex = mem.read8(messagePointer + 4);
const u32 bytes = mem.read16(messagePointer + 8);
// ...why do these parameters even exist?
const u16 width = mem.read16(messagePointer + 12);
const u16 height = mem.read16(messagePointer + 16);
const PortSelect port(portIndex);
log("CAM::SetTransferLines (port = %d, lines = %d, width = %d, height = %d)\n", port, lines, width, height);
if (port.isValid()) {
for (int i : port.getPortIndices()) {
ports[i].transferBytes = bytes;
}
} else {
Helpers::warn("CAM::SetTransferBytes: Invalid port\n");
}
log("CAM::SetTransferBytes (port = %d, bytes = %d, width = %d, height = %d)\n", portIndex, bytes, width, height);
mem.write32(messagePointer, IPC::responseHeader(0x9, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void CAMService::setTransferLines(u32 messagePointer) {
const u32 portIndex = mem.read8(messagePointer + 4);
const u16 lines = mem.read16(messagePointer + 8);
const u16 width = mem.read16(messagePointer + 12);
const u16 height = mem.read16(messagePointer + 16);
const PortSelect port(portIndex);
if (port.isValid()) {
const u32 transferBytes = lines * width * 2;
for (int i : port.getPortIndices()) {
ports[i].transferBytes = transferBytes;
}
} else {
Helpers::warn("CAM::SetTransferLines: Invalid port\n");
}
log("CAM::SetTransferLines (port = %d, lines = %d, width = %d, height = %d)\n", portIndex, lines, width, height);
mem.write32(messagePointer, IPC::responseHeader(0x9, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
@ -68,6 +172,41 @@ void CAMService::setFrameRate(u32 messagePointer) {
mem.write32(messagePointer + 4, Result::Success);
}
void CAMService::setSize(u32 messagePointer) {
const u32 cameraSelect = mem.read32(messagePointer + 4);
const u32 size = mem.read32(messagePointer + 8);
const u32 context = mem.read32(messagePointer + 12);
log("CAM::SetSize (camera select = %d, size = %d, context = %d)\n", cameraSelect, size, context);
mem.write32(messagePointer, IPC::responseHeader(0x1F, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void CAMService::setTrimming(u32 messagePointer) {
const u32 port = mem.read8(messagePointer + 4);
const bool trim = mem.read8(messagePointer + 8) != 0;
log("CAM::SetTrimming (port = %d, trimming = %s)\n", port, trim ? "enabled" : "disabled");
mem.write32(messagePointer, IPC::responseHeader(0x0E, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void CAMService::setTrimmingParamsCenter(u32 messagePointer) {
const u32 port = mem.read8(messagePointer + 4);
const s16 trimWidth = s16(mem.read16(messagePointer + 8));
const s16 trimHeight = s16(mem.read16(messagePointer + 12));
const s16 cameraWidth = s16(mem.read16(messagePointer + 16));
const s16 cameraHeight = s16(mem.read16(messagePointer + 20));
log("CAM::SetTrimmingParamsCenter (port = %d), trim size = (%d, %d), camera size = (%d, %d)\n", port, trimWidth, trimHeight, cameraWidth,
cameraHeight);
mem.write32(messagePointer, IPC::responseHeader(0x12, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
// Algorithm taken from Citra
// https://github.com/citra-emu/citra/blob/master/src/core/hle/service/cam/cam.cpp#L465
void CAMService::getMaxLines(u32 messagePointer) {
@ -100,16 +239,62 @@ void CAMService::getMaxLines(u32 messagePointer) {
}
}
void CAMService::getMaxBytes(u32 messagePointer) {
const u16 width = mem.read16(messagePointer + 4);
const u16 height = mem.read16(messagePointer + 8);
log("CAM::GetMaxBytes (width = %d, height = %d)\n", width, height);
constexpr u32 MIN_TRANSFER_UNIT = 256;
constexpr u32 MAX_BUFFER_SIZE = 2560;
if (width * height * 2 % MIN_TRANSFER_UNIT != 0) {
Helpers::panic("CAM::GetMaxLines out of range");
} else {
u32 bytes = MAX_BUFFER_SIZE;
while (width * height * 2 % bytes != 0) {
bytes -= MIN_TRANSFER_UNIT;
}
mem.write32(messagePointer, IPC::responseHeader(0xA, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write32(messagePointer + 8, bytes);
}
}
void CAMService::getSuitableY2RCoefficients(u32 messagePointer) {
log("CAM::GetSuitableY2RCoefficients\n");
mem.write32(messagePointer, IPC::responseHeader(0x36, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
// Y2R standard coefficient value
mem.write32(messagePointer + 8, 0);
}
void CAMService::getTransferBytes(u32 messagePointer) {
const u32 portIndex = mem.read8(messagePointer + 4);
const PortSelect port(portIndex);
log("CAM::GetTransferBytes (port = %d)\n", portIndex);
mem.write32(messagePointer, IPC::responseHeader(0x0C, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
if (port.isSinglePort()) {
mem.write32(messagePointer + 8, ports[port.getSingleIndex()].transferBytes);
} else {
// TODO: This should return the proper error code
Helpers::warn("CAM::GetTransferBytes: Invalid port index");
mem.write32(messagePointer + 8, 0);
}
}
void CAMService::getBufferErrorInterruptEvent(u32 messagePointer) {
const u32 port = mem.read32(messagePointer + 4);
log("CAM::GetBufferErrorInterruptEvent (port = %d)\n", port);
const u32 portIndex = mem.read8(messagePointer + 4);
const PortSelect port(portIndex);
log("CAM::GetBufferErrorInterruptEvent (port = %d)\n", portIndex);
mem.write32(messagePointer, IPC::responseHeader(0x6, 1, 2));
if (port >= portCount) {
Helpers::panic("CAM::GetBufferErrorInterruptEvent: Invalid port");
} else {
auto& event = bufferErrorInterruptEvents[port];
if (port.isSinglePort()) {
auto& event = ports[port.getSingleIndex()].bufferErrorInterruptEvent;
if (!event.has_value()) {
event = kernel.makeEvent(ResetType::OneShot);
}
@ -117,5 +302,55 @@ void CAMService::getBufferErrorInterruptEvent(u32 messagePointer) {
mem.write32(messagePointer + 4, Result::Success);
mem.write32(messagePointer + 8, 0);
mem.write32(messagePointer + 12, event.value());
} else {
Helpers::panic("CAM::GetBufferErrorInterruptEvent: Invalid port");
}
}
}
void CAMService::setReceiving(u32 messagePointer) {
const u32 destination = mem.read32(messagePointer + 4);
const u32 portIndex = mem.read8(messagePointer + 8);
const u32 size = mem.read32(messagePointer + 12);
const u16 transferUnit = mem.read16(messagePointer + 16);
const Handle process = mem.read32(messagePointer + 24);
const PortSelect port(portIndex);
log("CAM::SetReceiving (port = %d)\n", portIndex);
mem.write32(messagePointer, IPC::responseHeader(0x7, 1, 2));
if (port.isSinglePort()) {
auto& event = ports[port.getSingleIndex()].receiveEvent;
if (!event.has_value()) {
event = kernel.makeEvent(ResetType::OneShot);
}
mem.write32(messagePointer + 4, Result::Success);
mem.write32(messagePointer + 8, 0);
mem.write32(messagePointer + 12, event.value());
} else {
Helpers::panic("CAM::SetReceiving: Invalid port");
}
}
void CAMService::startCapture(u32 messagePointer) {
const u32 portIndex = mem.read8(messagePointer + 4);
const PortSelect port(portIndex);
log("CAM::StartCapture (port = %d)\n", portIndex);
mem.write32(messagePointer, IPC::responseHeader(0x01, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
if (port.isValid()) {
for (int i : port.getPortIndices()) {
auto& event = ports[port.getSingleIndex()].receiveEvent;
// Until we properly implement cameras, immediately signal the receive event
if (event.has_value()) {
kernel.signalEvent(event.value());
}
}
} else {
Helpers::warn("CAM::StartCapture: Invalid port index");
}
}

View file

@ -27,6 +27,7 @@ namespace FSCommands {
CloseArchive = 0x080E0080,
FormatThisUserSaveData = 0x080F0180,
GetFreeBytes = 0x08120080,
GetSdmcArchiveResource = 0x08140000,
IsSdmcDetected = 0x08170000,
IsSdmcWritable = 0x08180000,
CardSlotIsInserted = 0x08210000,
@ -96,6 +97,7 @@ ArchiveBase* FSService::getArchiveFromID(u32 id, const FSPath& archivePath) {
case ArchiveID::SystemSaveData: return &systemSaveData;
case ArchiveID::SDMC: return &sdmc;
case ArchiveID::SDMCWriteOnly: return &sdmcWriteOnly;
case ArchiveID::SavedataAndNcch: return &ncch; // This can only access NCCH outside of FSPXI
default:
Helpers::panic("Unknown archive. ID: %d\n", id);
@ -179,6 +181,7 @@ void FSService::handleSyncRequest(u32 messagePointer) {
case FSCommands::GetFreeBytes: getFreeBytes(messagePointer); break;
case FSCommands::GetFormatInfo: getFormatInfo(messagePointer); break;
case FSCommands::GetPriority: getPriority(messagePointer); break;
case FSCommands::GetSdmcArchiveResource: getSdmcArchiveResource(messagePointer); break;
case FSCommands::GetThisSaveDataSecureValue: getThisSaveDataSecureValue(messagePointer); break;
case FSCommands::Initialize: initialize(messagePointer); break;
case FSCommands::InitializeWithSdkVersion: initializeWithSdkVersion(messagePointer); break;
@ -670,6 +673,9 @@ void FSService::getThisSaveDataSecureValue(u32 messagePointer) {
mem.write32(messagePointer, IPC::responseHeader(0x86F, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write8(messagePointer + 8, 0); // Secure value does not exist
mem.write8(messagePointer + 12, 1); // TODO: What is this?
mem.write64(messagePointer + 16, 0); // Secure value
}
void FSService::setThisSaveDataSecureValue(u32 messagePointer) {
@ -760,4 +766,23 @@ void FSService::renameFile(u32 messagePointer) {
// Everything is OK, let's do the rename. Both archives should match so we don't need the dest anymore
const HorizonResult res = sourceArchive->archive->renameFile(sourcePath, destPath);
mem.write32(messagePointer + 4, static_cast<u32>(res));
}
void FSService::getSdmcArchiveResource(u32 messagePointer) {
log("FS::GetSdmcArchiveResource"); // For the time being, return the same stubbed archive resource for every media type
static constexpr ArchiveResource resource = {
.sectorSize = 512,
.clusterSize = 16_KB,
.partitionCapacityInClusters = 0x80000, // 0x80000 * 16 KB = 8GB
.freeSpaceInClusters = 0x80000, // Same here
};
mem.write32(messagePointer, IPC::responseHeader(0x814, 5, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write32(messagePointer + 8, resource.sectorSize);
mem.write32(messagePointer + 12, resource.clusterSize);
mem.write32(messagePointer + 16, resource.partitionCapacityInClusters);
mem.write32(messagePointer + 20, resource.freeSpaceInClusters);
}

View file

@ -15,8 +15,10 @@ namespace ServiceCommands {
FlushDataCache = 0x00080082,
SetLCDForceBlack = 0x000B0040,
TriggerCmdReqQueue = 0x000C0000,
ReleaseRight = 0x00170000,
ImportDisplayCaptureInfo = 0x00180000,
SaveVramSysArea = 0x00190000,
RestoreVramSysArea = 0x001A0000,
SetInternalPriorities = 0x001E0080,
StoreDataCache = 0x001F0082
};
@ -49,6 +51,8 @@ void GPUService::handleSyncRequest(u32 messagePointer) {
case ServiceCommands::FlushDataCache: flushDataCache(messagePointer); break;
case ServiceCommands::ImportDisplayCaptureInfo: importDisplayCaptureInfo(messagePointer); break;
case ServiceCommands::RegisterInterruptRelayQueue: registerInterruptRelayQueue(messagePointer); break;
case ServiceCommands::ReleaseRight: releaseRight(messagePointer); break;
case ServiceCommands::RestoreVramSysArea: restoreVramSysArea(messagePointer); break;
case ServiceCommands::SaveVramSysArea: saveVramSysArea(messagePointer); break;
case ServiceCommands::SetAxiConfigQoSMode: setAxiConfigQoSMode(messagePointer); break;
case ServiceCommands::SetBufferSwap: setBufferSwap(messagePointer); break;
@ -80,6 +84,16 @@ void GPUService::acquireRight(u32 messagePointer) {
mem.write32(messagePointer + 4, Result::Success);
}
void GPUService::releaseRight(u32 messagePointer) {
log("GSP::GPU::ReleaseRight\n");
if (privilegedProcess == currentPID) {
privilegedProcess = 0xFFFFFFFF;
}
mem.write32(messagePointer, IPC::responseHeader(0x17, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
// TODO: What is the flags field meant to be?
// What is the "GSP module thread index" meant to be?
// How does the shared memory handle thing work?
@ -131,8 +145,7 @@ void GPUService::requestInterrupt(GPUInterrupt type) {
// Not emulating this causes Yoshi's Wooly World, Captain Toad, Metroid 2 et al to hang
if (type == GPUInterrupt::VBlank0 || type == GPUInterrupt::VBlank1) {
int screen = static_cast<u32>(type) - static_cast<u32>(GPUInterrupt::VBlank0); // 0 for top screen, 1 for bottom
// TODO: Offset depends on GSP thread being triggered
FramebufferUpdate* update = reinterpret_cast<FramebufferUpdate*>(&sharedMem[0x200 + screen * sizeof(FramebufferUpdate)]);
FramebufferUpdate* update = getFramebufferInfo(screen);
if (update->dirtyFlag & 1) {
setBufferSwapImpl(screen, update->framebufferInfo[update->index]);
@ -470,10 +483,50 @@ void GPUService::saveVramSysArea(u32 messagePointer) {
mem.write32(messagePointer + 4, Result::Success);
}
void GPUService::restoreVramSysArea(u32 messagePointer) {
Helpers::warn("GSP::GPU::RestoreVramSysArea (stubbed)");
mem.write32(messagePointer, IPC::responseHeader(0x1A, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
// Used in similar fashion to the SaveVramSysArea function
void GPUService::importDisplayCaptureInfo(u32 messagePointer) {
Helpers::warn("GSP::GPU::ImportDisplayCaptureInfo (stubbed)");
mem.write32(messagePointer, IPC::responseHeader(0x18, 9, 0));
mem.write32(messagePointer + 4, Result::Success);
if (sharedMem == nullptr) {
Helpers::warn("GSP::GPU::ImportDisplayCaptureInfo called without GSP module being properly initialized!");
return;
}
FramebufferUpdate* topScreen = getTopFramebufferInfo();
FramebufferUpdate* bottomScreen = getBottomFramebufferInfo();
// Capture the relevant data for both screens and return them to the caller
CaptureInfo topScreenCapture = {
.leftFramebuffer = topScreen->framebufferInfo[topScreen->index].leftFramebufferVaddr,
.rightFramebuffer = topScreen->framebufferInfo[topScreen->index].rightFramebufferVaddr,
.format = topScreen->framebufferInfo[topScreen->index].format,
.stride = topScreen->framebufferInfo[topScreen->index].stride,
};
CaptureInfo bottomScreenCapture = {
.leftFramebuffer = bottomScreen->framebufferInfo[bottomScreen->index].leftFramebufferVaddr,
.rightFramebuffer = bottomScreen->framebufferInfo[bottomScreen->index].rightFramebufferVaddr,
.format = bottomScreen->framebufferInfo[bottomScreen->index].format,
.stride = bottomScreen->framebufferInfo[bottomScreen->index].stride,
};
mem.write32(messagePointer + 8, topScreenCapture.leftFramebuffer);
mem.write32(messagePointer + 12, topScreenCapture.rightFramebuffer);
mem.write32(messagePointer + 16, topScreenCapture.format);
mem.write32(messagePointer + 20, topScreenCapture.stride);
mem.write32(messagePointer + 24, bottomScreenCapture.leftFramebuffer);
mem.write32(messagePointer + 28, bottomScreenCapture.rightFramebuffer);
mem.write32(messagePointer + 32, bottomScreenCapture.format);
mem.write32(messagePointer + 36, bottomScreenCapture.stride);
}

View file

@ -120,9 +120,14 @@ void IRUserService::requireConnection(u32 messagePointer) {
u32 sharedMemAddress = sharedMemory.value().addr;
if (deviceID == u8(DeviceID::CirclePadPro)) {
mem.write8(sharedMemAddress + offsetof(SharedMemoryStatus, connectionStatus), 2); // Citra uses 2 here but only 1 works??
mem.write8(sharedMemAddress + offsetof(SharedMemoryStatus, connectionRole), 2);
mem.write8(sharedMemAddress + offsetof(SharedMemoryStatus, isConnected), 1);
// Note: We temporarily pretend we don't have a CirclePad Pro. This code must change when we emulate it or N3DS C-stick
constexpr u8 status = 1; // Not connected. Any value other than 2 is considered not connected.
constexpr u8 role = 0;
constexpr u8 connected = 0;
mem.write8(sharedMemAddress + offsetof(SharedMemoryStatus, connectionStatus), status);
mem.write8(sharedMemAddress + offsetof(SharedMemoryStatus, connectionRole), role);
mem.write8(sharedMemAddress + offsetof(SharedMemoryStatus, isConnected), connected);
connectedDevice = true;
if (connectionStatusEvent.has_value()) {

View file

@ -6,6 +6,7 @@ namespace PTMCommands {
GetAdapterState = 0x00050000,
GetBatteryLevel = 0x00070000,
GetBatteryChargeState = 0x00080000,
GetPedometerState = 0x00090000,
GetStepHistory = 0x000B00C2,
GetTotalStepCount = 0x000C0000,
GetStepHistoryAll = 0x000F0084,
@ -30,6 +31,7 @@ void PTMService::handleSyncRequest(u32 messagePointer, PTMService::Type type) {
case PTMCommands::GetAdapterState: getAdapterState(messagePointer); break;
case PTMCommands::GetBatteryChargeState: getBatteryChargeState(messagePointer); break;
case PTMCommands::GetBatteryLevel: getBatteryLevel(messagePointer); break;
case PTMCommands::GetPedometerState: getPedometerState(messagePointer); break;
case PTMCommands::GetStepHistory: getStepHistory(messagePointer); break;
case PTMCommands::GetStepHistoryAll: getStepHistoryAll(messagePointer); break;
case PTMCommands::GetTotalStepCount: getTotalStepCount(messagePointer); break;
@ -67,11 +69,20 @@ void PTMService::getBatteryChargeState(u32 messagePointer) {
// We're only charging if the battery is not already full
const bool charging = config.chargerPlugged && (config.batteryPercentage < 100);
mem.write32(messagePointer, IPC::responseHeader(0x7, 2, 0));
mem.write32(messagePointer, IPC::responseHeader(0x8, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write8(messagePointer + 8, charging ? 1 : 0);
}
void PTMService::getPedometerState(u32 messagePointer) {
log("PTM::GetPedometerState");
constexpr bool countingSteps = true;
mem.write32(messagePointer, IPC::responseHeader(0x9, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write8(messagePointer + 8, countingSteps ? 1 : 0);
}
void PTMService::getBatteryLevel(u32 messagePointer) {
log("PTM::GetBatteryLevel");

View file

@ -18,12 +18,16 @@ namespace Y2RCommands {
SetSendingY = 0x00100102,
SetSendingU = 0x00110102,
SetSendingV = 0x00120102,
SetSendingYUV = 0x00130102,
SetReceiving = 0x00180102,
SetInputLineWidth = 0x001A0040,
GetInputLineWidth = 0x001B0000,
SetInputLines = 0x001C0040,
GetInputLines = 0x001D0000,
SetCoefficientParams = 0x001E0100,
GetCoefficientParams = 0x001F0000,
SetStandardCoeff = 0x00200040,
GetStandardCoefficientParams = 0x00210040,
SetAlpha = 0x00220040,
StartConversion = 0x00260000,
StopConversion = 0x00270000,
@ -50,6 +54,8 @@ void Y2RService::reset() {
alpha = 0xFFFF;
inputLines = 69;
inputLineWidth = 420;
conversionCoefficients.fill(0);
}
void Y2RService::handleSyncRequest(u32 messagePointer) {
@ -62,6 +68,7 @@ void Y2RService::handleSyncRequest(u32 messagePointer) {
case Y2RCommands::GetInputLineWidth: getInputLineWidth(messagePointer); break;
case Y2RCommands::GetOutputFormat: getOutputFormat(messagePointer); break;
case Y2RCommands::GetTransferEndEvent: getTransferEndEvent(messagePointer); break;
case Y2RCommands::GetStandardCoefficientParams: getStandardCoefficientParams(messagePointer); break;
case Y2RCommands::IsBusyConversion: isBusyConversion(messagePointer); break;
case Y2RCommands::PingProcess: pingProcess(messagePointer); break;
case Y2RCommands::SetAlpha: setAlpha(messagePointer); break;
@ -76,12 +83,17 @@ void Y2RService::handleSyncRequest(u32 messagePointer) {
case Y2RCommands::SetSendingY: setSendingY(messagePointer); break;
case Y2RCommands::SetSendingU: setSendingU(messagePointer); break;
case Y2RCommands::SetSendingV: setSendingV(messagePointer); break;
case Y2RCommands::SetSendingYUV: setSendingYUV(messagePointer); break;
case Y2RCommands::SetSpacialDithering: setSpacialDithering(messagePointer); break;
case Y2RCommands::SetStandardCoeff: setStandardCoeff(messagePointer); break;
case Y2RCommands::SetTemporalDithering: setTemporalDithering(messagePointer); break;
case Y2RCommands::SetTransferEndInterrupt: setTransferEndInterrupt(messagePointer); break;
case Y2RCommands::StartConversion: [[likely]] startConversion(messagePointer); break;
case Y2RCommands::StopConversion: stopConversion(messagePointer); break;
// Intentionally break ordering a bit for less-used Y2R functions
case Y2RCommands::SetCoefficientParams: setCoefficientParams(messagePointer); break;
case Y2RCommands::GetCoefficientParams: getCoefficientParams(messagePointer); break;
default: Helpers::panic("Y2R service requested. Command: %08X\n", command);
}
}
@ -97,6 +109,8 @@ void Y2RService::driverInitialize(u32 messagePointer) {
log("Y2R::DriverInitialize\n");
mem.write32(messagePointer, IPC::responseHeader(0x2B, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
conversionCoefficients.fill(0);
}
void Y2RService::driverFinalize(u32 messagePointer) {
@ -276,6 +290,7 @@ void Y2RService::getInputLineWidth(u32 messagePointer) {
mem.write32(messagePointer + 4, Result::Success);
mem.write32(messagePointer + 8, inputLineWidth);
}
void Y2RService::setInputLines(u32 messagePointer) {
const u16 lines = mem.read16(messagePointer + 4);
log("Y2R::SetInputLines (lines = %d)\n", lines);
@ -306,7 +321,7 @@ void Y2RService::setStandardCoeff(u32 messagePointer) {
log("Y2R::SetStandardCoeff (coefficient = %d)\n", coeff);
mem.write32(messagePointer, IPC::responseHeader(0x20, 1, 0));
if (coeff > 3) {
if (coeff > 3) { // Invalid coefficient, should have an error code
Helpers::panic("Y2R: Invalid standard coefficient (coefficient = %d)\n", coeff);
}
@ -316,6 +331,52 @@ void Y2RService::setStandardCoeff(u32 messagePointer) {
}
}
void Y2RService::getStandardCoefficientParams(u32 messagePointer) {
const u32 coefficientIndex = mem.read32(messagePointer + 4);
log("Y2R::GetStandardCoefficientParams (coefficient = %d)\n", coefficientIndex);
if (coefficientIndex > 3) { // Invalid coefficient, should have an error code
Helpers::panic("Y2R: Invalid standard coefficient (coefficient = %d)\n", coefficientIndex);
} else {
mem.write32(messagePointer, IPC::responseHeader(0x21, 5, 0));
mem.write32(messagePointer + 4, Result::Success);
const auto& coeff = standardCoefficients[coefficientIndex];
// Write standard coefficient parameters to output buffer
for (int i = 0; i < 8; i++) {
const u32 pointer = messagePointer + 8 + i * sizeof(u16); // Pointer to write parameter to
mem.write16(pointer, coeff[i]);
}
}
}
void Y2RService::setCoefficientParams(u32 messagePointer) {
log("Y2R::SetCoefficientParams\n");
auto& coeff = conversionCoefficients;
// Write coefficient parameters to output buffer
for (int i = 0; i < 8; i++) {
const u32 pointer = messagePointer + 4 + i * sizeof(u16); // Pointer to write parameter to
coeff[i] = mem.read16(pointer);
}
mem.write32(messagePointer, IPC::responseHeader(0x1E, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void Y2RService::getCoefficientParams(u32 messagePointer) {
log("Y2R::GetCoefficientParams\n");
mem.write32(messagePointer, IPC::responseHeader(0x1F, 5, 0));
mem.write32(messagePointer + 4, Result::Success);
const auto& coeff = conversionCoefficients;
// Write coefficient parameters to output buffer
for (int i = 0; i < 8; i++) {
const u32 pointer = messagePointer + 8 + i * sizeof(u16); // Pointer to write parameter to
mem.write16(pointer, coeff[i]);
}
}
void Y2RService::setSendingY(u32 messagePointer) {
log("Y2R::SetSendingY\n");
Helpers::warn("Unimplemented Y2R::SetSendingY");
@ -340,6 +401,14 @@ void Y2RService::setSendingV(u32 messagePointer) {
mem.write32(messagePointer + 4, Result::Success);
}
void Y2RService::setSendingYUV(u32 messagePointer) {
log("Y2R::SetSendingYUV\n");
Helpers::warn("Unimplemented Y2R::SetSendingYUV");
mem.write32(messagePointer, IPC::responseHeader(0x13, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void Y2RService::setReceiving(u32 messagePointer) {
log("Y2R::SetReceiving\n");
Helpers::warn("Unimplemented Y2R::setReceiving");

View file

@ -1,6 +1,8 @@
#include "emulator.hpp"
#ifndef __ANDROID__
#include <SDL_filesystem.h>
#endif
#include <fstream>
@ -15,10 +17,11 @@ __declspec(dllexport) DWORD AmdPowerXpressRequestHighPerformance = 1;
#endif
Emulator::Emulator()
: config(std::filesystem::current_path() / "config.toml"), kernel(cpu, memory, gpu, config), cpu(memory, kernel), gpu(memory, config),
memory(cpu.getTicksRef(), config), cheats(memory, kernel.getServiceManager().getHID()), lua(memory), running(false), programRunning(false)
: config(getConfigPath()), kernel(cpu, memory, gpu, config), cpu(memory, kernel, *this), gpu(memory, config), memory(cpu.getTicksRef(), config),
cheats(memory, kernel.getServiceManager().getHID()), lua(memory), running(false), programRunning(false)
#ifdef PANDA3DS_ENABLE_HTTP_SERVER
, httpServer(this)
,
httpServer(this)
#endif
{
#ifdef PANDA3DS_ENABLE_DISCORD_RPC
@ -31,7 +34,7 @@ Emulator::Emulator()
}
Emulator::~Emulator() {
config.save(std::filesystem::current_path() / "config.toml");
config.save();
lua.close();
#ifdef PANDA3DS_ENABLE_DISCORD_RPC
@ -43,6 +46,9 @@ void Emulator::reset(ReloadOption reload) {
cpu.reset();
gpu.reset();
memory.reset();
// Reset scheduler and add a VBlank event
scheduler.reset();
// Kernel must be reset last because it depends on CPU/Memory state
kernel.reset();
@ -68,6 +74,23 @@ void Emulator::reset(ReloadOption reload) {
}
}
std::filesystem::path Emulator::getAndroidAppPath() {
// SDL_GetPrefPath fails to get the path due to no JNI environment
std::ifstream cmdline("/proc/self/cmdline");
std::string applicationName;
std::getline(cmdline, applicationName, '\0');
return std::filesystem::path("/data") / "data" / applicationName / "files";
}
std::filesystem::path Emulator::getConfigPath() {
if constexpr (Helpers::isAndroid()) {
return getAndroidAppPath() / "config.toml";
} else {
return std::filesystem::current_path() / "config.toml";
}
}
void Emulator::step() {}
void Emulator::render() {}
@ -80,12 +103,6 @@ void Emulator::runFrame() {
if (running) {
cpu.runFrame(); // Run 1 frame of instructions
gpu.display(); // Display graphics
lua.signalEvent(LuaEvent::Frame);
// Send VBlank interrupts
ServiceManager& srv = kernel.getServiceManager();
srv.sendGPUInterrupt(GPUInterrupt::VBlank0);
srv.sendGPUInterrupt(GPUInterrupt::VBlank1);
// Run cheats if any are loaded
if (cheats.haveCheats()) [[unlikely]] {
@ -98,6 +115,67 @@ void Emulator::runFrame() {
}
}
void Emulator::pollScheduler() {
auto& events = scheduler.events;
// Pop events until there's none pending anymore
while (scheduler.currentTimestamp >= scheduler.nextTimestamp) {
// Read event timestamp and type, pop it from the scheduler and handle it
auto [time, eventType] = std::move(*events.begin());
events.erase(events.begin());
scheduler.updateNextTimestamp();
switch (eventType) {
case Scheduler::EventType::VBlank: [[likely]] {
// Signal that we've reached the end of a frame
frameDone = true;
lua.signalEvent(LuaEvent::Frame);
// Send VBlank interrupts
ServiceManager& srv = kernel.getServiceManager();
srv.sendGPUInterrupt(GPUInterrupt::VBlank0);
srv.sendGPUInterrupt(GPUInterrupt::VBlank1);
// Queue next VBlank event
scheduler.addEvent(Scheduler::EventType::VBlank, time + CPU::ticksPerSec / 60);
break;
}
case Scheduler::EventType::UpdateTimers: kernel.pollTimers(); break;
default: {
Helpers::panic("Scheduler: Unimplemented event type received: %d\n", static_cast<int>(eventType));
break;
}
}
}
}
// Get path for saving files (AppData on Windows, /home/user/.local/share/ApplicationName on Linux, etc)
// Inside that path, we be use a game-specific folder as well. Eg if we were loading a ROM called PenguinDemo.3ds, the savedata would be in
// %APPDATA%/Alber/PenguinDemo/SaveData on Windows, and so on. We do this because games save data in their own filesystem on the cart.
// If the portable build setting is enabled, then those saves go in the executable directory instead
std::filesystem::path Emulator::getAppDataRoot() {
std::filesystem::path appDataPath;
#ifdef __ANDROID__
appDataPath = getAndroidAppPath();
#else
char* appData;
if (!config.usePortableBuild) {
appData = SDL_GetPrefPath(nullptr, "Alber");
appDataPath = std::filesystem::path(appData);
} else {
appData = SDL_GetBasePath();
appDataPath = std::filesystem::path(appData) / "Emulator Files";
}
SDL_free(appData);
#endif
return appDataPath;
}
bool Emulator::loadROM(const std::filesystem::path& path) {
// Reset the emulator if we've already loaded a ROM
if (romType != ROMType::None) {
@ -108,30 +186,7 @@ bool Emulator::loadROM(const std::filesystem::path& path) {
memory.loadedCXI = std::nullopt;
memory.loaded3DSX = std::nullopt;
// Get path for saving files (AppData on Windows, /home/user/.local/share/ApplicationName on Linux, etc)
// Inside that path, we be use a game-specific folder as well. Eg if we were loading a ROM called PenguinDemo.3ds, the savedata would be in
// %APPDATA%/Alber/PenguinDemo/SaveData on Windows, and so on. We do this because games save data in their own filesystem on the cart.
// If the portable build setting is enabled, then those saves go in the executable directory instead
std::filesystem::path appDataPath;
#ifdef __ANDROID__
// SDL_GetPrefPath fails to get the path due to no JNI environment
std::ifstream cmdline("/proc/self/cmdline");
std::string applicationName;
std::getline(cmdline, applicationName, '\0');
appDataPath = std::filesystem::path("/data") / "data" / applicationName / "files";
#else
char* appData;
if (!config.usePortableBuild) {
appData = SDL_GetPrefPath(nullptr, "Alber");
appDataPath = std::filesystem::path(appData);
} else {
appData = SDL_GetBasePath();
appDataPath = std::filesystem::path(appData) / "Emulator Files";
}
SDL_free(appData);
#endif
const std::filesystem::path appDataPath = getAppDataRoot();
const std::filesystem::path dataPath = appDataPath / path.filename().stem();
const std::filesystem::path aesKeysPath = appDataPath / "sysdata" / "aes_keys.txt";
IOFile::setAppDataDir(dataPath);
@ -237,6 +292,17 @@ bool Emulator::loadELF(std::ifstream& file) {
return true;
}
std::span<u8> Emulator::getSMDH() {
switch (romType) {
case ROMType::NCSD:
case ROMType::CXI:
return memory.getCXI()->smdh;
default: {
return std::span<u8>();
}
}
}
#ifdef PANDA3DS_ENABLE_DISCORD_RPC
void Emulator::updateDiscord() {
if (config.discordRpcEnabled) {

View file

@ -4,8 +4,13 @@
#include <stdexcept>
#include "hydra_icon.hpp"
#include "swap.hpp"
class HC_GLOBAL HydraCore final : public hydra::IBase, public hydra::IOpenGlRendered, public hydra::IFrontendDriven, public hydra::IInput {
class HC_GLOBAL HydraCore final : public hydra::IBase,
public hydra::IOpenGlRendered,
public hydra::IFrontendDriven,
public hydra::IInput,
public hydra::ICheat {
HYDRA_CLASS
public:
HydraCore();
@ -18,22 +23,30 @@ class HC_GLOBAL HydraCore final : public hydra::IBase, public hydra::IOpenGlRend
void setOutputSize(hydra::Size size) override;
// IOpenGlRendered
void resetContext() override;
void destroyContext() override;
void setFbo(unsigned handle) override;
void setContext(void* context) override;
void setGetProcAddress(void* function) override;
// IFrontendDriven
void runFrame() override;
uint16_t getFps() override;
u16 getFps() override;
// IInput
void setPollInputCallback(void (*callback)()) override;
void setCheckButtonCallback(int32_t (*callback)(uint32_t player, hydra::ButtonType button)) override;
void setCheckButtonCallback(s32 (*callback)(u32 player, hydra::ButtonType button)) override;
// ICheat
u32 addCheat(const u8* data, u32 size) override;
void removeCheat(u32 id) override;
void enableCheat(u32 id) override;
void disableCheat(u32 id) override;
std::unique_ptr<Emulator> emulator;
RendererGL* renderer;
void (*pollInputCallback)() = nullptr;
int32_t (*checkButtonCallback)(uint32_t player, hydra::ButtonType button) = nullptr;
void* getProcAddress = nullptr;
};
HydraCore::HydraCore() : emulator(new Emulator) {
@ -88,11 +101,10 @@ void HydraCore::runFrame() {
}
hid.updateInputs(emulator->getTicks());
emulator->runFrame();
}
uint16_t HydraCore::getFps() { return 60; }
u16 HydraCore::getFps() { return 60; }
void HydraCore::reset() { emulator->reset(Emulator::ReloadOption::Reload); }
hydra::Size HydraCore::getNativeSize() { return {400, 480}; }
@ -100,13 +112,13 @@ hydra::Size HydraCore::getNativeSize() { return {400, 480}; }
// Size doesn't matter as the glBlitFramebuffer call is commented out for the core
void HydraCore::setOutputSize(hydra::Size size) {}
void HydraCore::setGetProcAddress(void* function) {
void HydraCore::resetContext() {
#ifdef __ANDROID__
if (!gladLoadGLES2Loader(reinterpret_cast<GLADloadproc>(function))) {
if (!gladLoadGLES2Loader(reinterpret_cast<GLADloadproc>(getProcAddress))) {
Helpers::panic("OpenGL ES init failed");
}
#else
if (!gladLoadGLLoader(reinterpret_cast<GLADloadproc>(function))) {
if (!gladLoadGLLoader(reinterpret_cast<GLADloadproc>(getProcAddress))) {
Helpers::panic("OpenGL init failed");
}
#endif
@ -114,13 +126,22 @@ void HydraCore::setGetProcAddress(void* function) {
emulator->initGraphicsContext(nullptr);
}
void HydraCore::setContext(void*) {}
void HydraCore::destroyContext() { emulator->deinitGraphicsContext(); }
void HydraCore::setFbo(unsigned handle) { renderer->setFBO(handle); }
void HydraCore::setGetProcAddress(void* function) { getProcAddress = function; }
void HydraCore::setPollInputCallback(void (*callback)()) { pollInputCallback = callback; }
void HydraCore::setCheckButtonCallback(int32_t (*callback)(uint32_t player, hydra::ButtonType button)) { checkButtonCallback = callback; }
void HydraCore::setCheckButtonCallback(s32 (*callback)(u32 player, hydra::ButtonType button)) { checkButtonCallback = callback; }
HC_API hydra::IBase* createEmulator() { return new HydraCore; }
u32 HydraCore::addCheat(const u8* data, u32 size) {
return emulator->getCheats().addCheat(data, size);
};
void HydraCore::removeCheat(u32 id) { emulator->getCheats().removeCheat(id); }
void HydraCore::enableCheat(u32 id) { emulator->getCheats().enableCheat(id); }
void HydraCore::disableCheat(u32 id) { emulator->getCheats().disableCheat(id); }
HC_API hydra::IBase* createEmulator() { return new HydraCore(); }
HC_API void destroyEmulator(hydra::IBase* emulator) { delete emulator; }
HC_API const char* getInfo(hydra::InfoType type) {
@ -140,4 +161,4 @@ HC_API const char* getInfo(hydra::InfoType type) {
default: return nullptr;
}
}
}

116
src/jni_driver.cpp Normal file
View file

@ -0,0 +1,116 @@
#include <EGL/egl.h>
#include <android/log.h>
#include <jni.h>
#include <stdexcept>
#include "emulator.hpp"
#include "renderer_gl/renderer_gl.hpp"
#include "services/hid.hpp"
std::unique_ptr<Emulator> emulator = nullptr;
HIDService* hidService = nullptr;
RendererGL* renderer = nullptr;
bool romLoaded = false;
JavaVM* jvm = nullptr;
#define AlberFunction(type, name) JNIEXPORT type JNICALL Java_com_panda3ds_pandroid_AlberDriver_##name
void throwException(JNIEnv* env, const char* message) {
jclass exceptionClass = env->FindClass("java/lang/RuntimeException");
env->ThrowNew(exceptionClass, message);
}
JNIEnv* jniEnv() {
JNIEnv* env;
auto status = jvm->GetEnv((void**)&env, JNI_VERSION_1_6);
if (status == JNI_EDETACHED) {
jvm->AttachCurrentThread(&env, nullptr);
} else if (status != JNI_OK) {
throw std::runtime_error("Failed to obtain JNIEnv from JVM!!");
}
return env;
}
extern "C" {
#define MAKE_SETTING(functionName, type, settingName) \
AlberFunction(void, functionName) (JNIEnv* env, jobject obj, type value) { emulator->getConfig().settingName = value; }
MAKE_SETTING(setShaderJitEnabled, jboolean, shaderJitEnabled)
#undef MAKE_SETTING
AlberFunction(void, Setup)(JNIEnv* env, jobject obj) { env->GetJavaVM(&jvm); }
AlberFunction(void, Pause)(JNIEnv* env, jobject obj) { emulator->pause(); }
AlberFunction(void, Resume)(JNIEnv* env, jobject obj) { emulator->resume(); }
AlberFunction(void, Initialize)(JNIEnv* env, jobject obj) {
emulator = std::make_unique<Emulator>();
if (emulator->getRendererType() != RendererType::OpenGL) {
return throwException(env, "Renderer type is not OpenGL");
}
renderer = static_cast<RendererGL*>(emulator->getRenderer());
hidService = &emulator->getServiceManager().getHID();
if (!gladLoadGLES2Loader(reinterpret_cast<GLADloadproc>(eglGetProcAddress))) {
return throwException(env, "Failed to load OpenGL ES 2.0");
}
__android_log_print(ANDROID_LOG_INFO, "AlberDriver", "OpenGL ES %d.%d", GLVersion.major, GLVersion.minor);
emulator->initGraphicsContext(nullptr);
}
AlberFunction(void, RunFrame)(JNIEnv* env, jobject obj, jint fbo) {
renderer->setFBO(fbo);
// TODO: don't reset entire state manager
renderer->resetStateManager();
emulator->runFrame();
hidService->updateInputs(emulator->getTicks());
}
AlberFunction(void, Finalize)(JNIEnv* env, jobject obj) {
emulator = nullptr;
hidService = nullptr;
renderer = nullptr;
}
AlberFunction(jboolean, HasRomLoaded)(JNIEnv* env, jobject obj) { return romLoaded; }
AlberFunction(void, LoadRom)(JNIEnv* env, jobject obj, jstring path) {
const char* pathStr = env->GetStringUTFChars(path, nullptr);
romLoaded = emulator->loadROM(pathStr);
env->ReleaseStringUTFChars(path, pathStr);
}
AlberFunction(void, LoadLuaScript)(JNIEnv* env, jobject obj, jstring script) {
const char* scriptStr = env->GetStringUTFChars(script, nullptr);
emulator->getLua().loadString(scriptStr);
env->ReleaseStringUTFChars(script, scriptStr);
}
AlberFunction(void, TouchScreenDown)(JNIEnv* env, jobject obj, jint x, jint y) { hidService->setTouchScreenPress((u16)x, (u16)y); }
AlberFunction(void, TouchScreenUp)(JNIEnv* env, jobject obj) { hidService->releaseTouchScreen(); }
AlberFunction(void, KeyUp)(JNIEnv* env, jobject obj, jint keyCode) { hidService->releaseKey((u32)keyCode); }
AlberFunction(void, KeyDown)(JNIEnv* env, jobject obj, jint keyCode) { hidService->pressKey((u32)keyCode); }
AlberFunction(void, SetCirclepadAxis)(JNIEnv* env, jobject obj, jint x, jint y) {
hidService->setCirclepadX((s16)x);
hidService->setCirclepadY((s16)y);
}
AlberFunction(jbyteArray, GetSmdh)(JNIEnv* env, jobject obj) {
std::span<u8> smdh = emulator->getSMDH();
jbyteArray result = env->NewByteArray(smdh.size());
env->SetByteArrayRegion(result, 0, smdh.size(), (jbyte*)smdh.data());
return result;
}
}
#undef AlberFunction

View file

@ -58,6 +58,31 @@ void LuaManager::loadFile(const char* path) {
}
}
void LuaManager::loadString(const std::string& code) {
// Initialize Lua if it has not been initialized
if (!initialized) {
initialize();
}
// If init failed, don't execute
if (!initialized) {
printf("Lua initialization failed, file won't run\n");
haveScript = false;
return;
}
int status = luaL_loadstring(L, code.c_str()); // load Lua script
int ret = lua_pcall(L, 0, 0, 0); // tell Lua to run the script
if (ret != 0) {
haveScript = false;
fprintf(stderr, "%s\n", lua_tostring(L, -1)); // tell us what mistake we made
} else {
haveScript = true;
}
}
void LuaManager::signalEventInternal(LuaEvent e) {
lua_getglobal(L, "eventHandler"); // We want to call the event handler
lua_pushnumber(L, static_cast<int>(e)); // Push event type

View file

@ -0,0 +1,63 @@
#include "panda_qt/about_window.hpp"
#include <QLabel>
#include <QTextEdit>
#include <QVBoxLayout>
#include <QtGlobal>
// Based on https://github.com/dolphin-emu/dolphin/blob/master/Source/Core/DolphinQt/AboutDialog.cpp
AboutWindow::AboutWindow(QWidget* parent) : QDialog(parent) {
resize(200, 200);
setWindowTitle(tr("About Panda3DS"));
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
const QString text =
QStringLiteral(R"(
<p style='font-size:38pt; font-weight:400;'>Panda3DS</p>
<p>
%ABOUT_PANDA3DS%<br>
<a href='https://panda3ds.com/'>%SUPPORT%</a><br>
</p>
<p>
<a>%AUTHORS%</a>
</p>
)")
.replace(QStringLiteral("%ABOUT_PANDA3DS%"), tr("Panda3DS is a free and open source Nintendo 3DS emulator, for Windows, MacOS and Linux"))
.replace(QStringLiteral("%SUPPORT%"), tr("Visit panda3ds.com for help with Panda3DS and links to our official support sites."))
.replace(
QStringLiteral("%AUTHORS%"), tr("Panda3DS is developed by volunteers in their spare time. Below is a list of some of these"
" volunteers who've agreed to be listed here, in no particular order.<br>If you think you should be "
"listed here too, please inform us<br><br>"
"- Peach (wheremyfoodat)<br>"
"- noumidev<br>"
"- liuk707<br>"
"- Wunk<br>"
"- marysaka<br>"
"- Sky<br>"
"- merryhime<br>"
"- TGP17<br>"
"- Shadow<br>")
);
QLabel* textLabel = new QLabel(text);
textLabel->setTextInteractionFlags(Qt::TextBrowserInteraction);
textLabel->setOpenExternalLinks(true);
QLabel* logo = new QLabel();
logo->setPixmap(QPixmap(":/docs/img/rstarstruck_icon.png"));
logo->setContentsMargins(30, 0, 30, 0);
QVBoxLayout* mainLayout = new QVBoxLayout;
QHBoxLayout* hLayout = new QHBoxLayout;
setLayout(mainLayout);
mainLayout->addLayout(hLayout);
hLayout->setAlignment(Qt::AlignLeft);
hLayout->addWidget(logo);
hLayout->addWidget(textLabel);
}

View file

@ -0,0 +1,268 @@
#include "panda_qt/cheats_window.hpp"
#include <QCheckBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QPushButton>
#include <QTextEdit>
#include <QVBoxLayout>
#include <QTimer>
#include <functional>
#include "cheats.hpp"
#include "emulator.hpp"
#include "panda_qt/main_window.hpp"
MainWindow* mainWindow = nullptr;
struct CheatMetadata {
u32 handle = Cheats::badCheatHandle;
std::string name = "New cheat";
std::string code;
bool enabled = true;
};
void dispatchToMainThread(std::function<void()> callback) {
QTimer* timer = new QTimer();
timer->moveToThread(qApp->thread());
timer->setSingleShot(true);
QObject::connect(timer, &QTimer::timeout, [=]()
{
callback();
timer->deleteLater();
});
QMetaObject::invokeMethod(timer, "start", Qt::QueuedConnection, Q_ARG(int, 0));
}
class CheatEntryWidget : public QWidget {
public:
CheatEntryWidget(Emulator* emu, CheatMetadata metadata, QListWidget* parent);
void Update() {
name->setText(metadata.name.c_str());
enabled->setChecked(metadata.enabled);
update();
}
void Remove() {
emu->getCheats().removeCheat(metadata.handle);
cheatList->takeItem(cheatList->row(listItem));
deleteLater();
}
const CheatMetadata& getMetadata() { return metadata; }
void setMetadata(const CheatMetadata& metadata) { this->metadata = metadata; }
private:
void checkboxChanged(int state);
void editClicked();
Emulator* emu;
CheatMetadata metadata;
u32 handle;
QLabel* name;
QCheckBox* enabled;
QListWidget* cheatList;
QListWidgetItem* listItem;
};
class CheatEditDialog : public QDialog {
public:
CheatEditDialog(Emulator* emu, CheatEntryWidget& cheatEntry);
void accepted();
void rejected();
private:
Emulator* emu;
CheatEntryWidget& cheatEntry;
QTextEdit* codeEdit;
QLineEdit* nameEdit;
};
CheatEntryWidget::CheatEntryWidget(Emulator* emu, CheatMetadata metadata, QListWidget* parent)
: QWidget(), emu(emu), metadata(metadata), cheatList(parent) {
QHBoxLayout* layout = new QHBoxLayout;
enabled = new QCheckBox;
enabled->setChecked(metadata.enabled);
name = new QLabel(metadata.name.c_str());
QPushButton* buttonEdit = new QPushButton(tr("Edit"));
connect(enabled, &QCheckBox::stateChanged, this, &CheatEntryWidget::checkboxChanged);
connect(buttonEdit, &QPushButton::clicked, this, &CheatEntryWidget::editClicked);
layout->addWidget(enabled);
layout->addWidget(name);
layout->addWidget(buttonEdit);
setLayout(layout);
listItem = new QListWidgetItem;
listItem->setSizeHint(sizeHint());
parent->addItem(listItem);
parent->setItemWidget(listItem, this);
}
void CheatEntryWidget::checkboxChanged(int state) {
bool enabled = state == Qt::Checked;
if (metadata.handle == Cheats::badCheatHandle) {
printf("Cheat handle is bad, this shouldn't happen\n");
return;
}
if (enabled) {
emu->getCheats().enableCheat(metadata.handle);
metadata.enabled = true;
} else {
emu->getCheats().disableCheat(metadata.handle);
metadata.enabled = false;
}
}
void CheatEntryWidget::editClicked() {
CheatEditDialog* dialog = new CheatEditDialog(emu, *this);
dialog->show();
}
CheatEditDialog::CheatEditDialog(Emulator* emu, CheatEntryWidget& cheatEntry) : QDialog(), emu(emu), cheatEntry(cheatEntry) {
setAttribute(Qt::WA_DeleteOnClose);
setModal(true);
QVBoxLayout* layout = new QVBoxLayout;
const CheatMetadata& metadata = cheatEntry.getMetadata();
codeEdit = new QTextEdit;
nameEdit = new QLineEdit;
nameEdit->setText(metadata.name.c_str());
nameEdit->setPlaceholderText(tr("Cheat name"));
layout->addWidget(nameEdit);
QFont font;
font.setFamily("Courier");
font.setFixedPitch(true);
font.setPointSize(10);
codeEdit->setFont(font);
if (metadata.code.size() != 0) {
// Nicely format it like so:
// 01234567 89ABCDEF
// 01234567 89ABCDEF
std::string formattedCode;
for (size_t i = 0; i < metadata.code.size(); i += 2) {
if (i != 0) {
if (i % 8 == 0 && i % 16 != 0) {
formattedCode += " ";
} else if (i % 16 == 0) {
formattedCode += "\n";
}
}
formattedCode += metadata.code[i];
formattedCode += metadata.code[i + 1];
}
codeEdit->setText(formattedCode.c_str());
}
layout->addWidget(codeEdit);
setLayout(layout);
auto buttons = QDialogButtonBox::Ok | QDialogButtonBox::Cancel;
QDialogButtonBox* buttonBox = new QDialogButtonBox(buttons);
layout->addWidget(buttonBox);
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(this, &QDialog::rejected, this, &CheatEditDialog::rejected);
connect(this, &QDialog::accepted, this, &CheatEditDialog::accepted);
}
void CheatEditDialog::accepted() {
QString code = codeEdit->toPlainText();
code.replace(QRegularExpression("[^0-9a-fA-F]"), "");
CheatMetadata metadata = cheatEntry.getMetadata();
metadata.name = nameEdit->text().toStdString();
metadata.code = code.toStdString();
cheatEntry.setMetadata(metadata);
std::vector<u8> bytes;
for (size_t i = 0; i < metadata.code.size(); i += 2) {
std::string hex = metadata.code.substr(i, 2);
bytes.push_back((u8)std::stoul(hex, nullptr, 16));
}
mainWindow->editCheat(cheatEntry.getMetadata().handle, bytes, [this](u32 handle) {
dispatchToMainThread([this, handle]() {
if (handle == Cheats::badCheatHandle) {
cheatEntry.Remove();
return;
} else {
CheatMetadata metadata = cheatEntry.getMetadata();
metadata.handle = handle;
cheatEntry.setMetadata(metadata);
cheatEntry.Update();
}
});
});
}
void CheatEditDialog::rejected() {
bool isEditing = cheatEntry.getMetadata().handle != Cheats::badCheatHandle;
if (!isEditing) {
// Was adding a cheat but user pressed cancel
cheatEntry.Remove();
}
}
CheatsWindow::CheatsWindow(Emulator* emu, const std::filesystem::path& cheatPath, QWidget* parent)
: QWidget(parent, Qt::Window), emu(emu), cheatPath(cheatPath) {
mainWindow = static_cast<MainWindow*>(parent);
QVBoxLayout* layout = new QVBoxLayout;
layout->setContentsMargins(6, 6, 6, 6);
setLayout(layout);
cheatList = new QListWidget;
layout->addWidget(cheatList);
QWidget* buttonBox = new QWidget;
QHBoxLayout* buttonLayout = new QHBoxLayout;
QPushButton* buttonAdd = new QPushButton(tr("Add"));
QPushButton* buttonRemove = new QPushButton(tr("Remove"));
connect(buttonAdd, &QPushButton::clicked, this, &CheatsWindow::addEntry);
connect(buttonRemove, &QPushButton::clicked, this, &CheatsWindow::removeClicked);
buttonLayout->addWidget(buttonAdd);
buttonLayout->addWidget(buttonRemove);
buttonBox->setLayout(buttonLayout);
layout->addWidget(buttonBox);
// TODO: load cheats from saved cheats per game
// for (const CheatMetadata& metadata : getSavedCheats())
// {
// new CheatEntryWidget(emu, metadata, cheatList);
// }
}
void CheatsWindow::addEntry() {
// CheatEntryWidget is added to the list when it's created
CheatEntryWidget* entry = new CheatEntryWidget(emu, {Cheats::badCheatHandle, "New cheat", "", true}, cheatList);
CheatEditDialog* dialog = new CheatEditDialog(emu, *entry);
dialog->show();
}
void CheatsWindow::removeClicked() {
QListWidgetItem* item = cheatList->currentItem();
if (item == nullptr) {
return;
}
CheatEntryWidget* entry = static_cast<CheatEntryWidget*>(cheatList->itemWidget(item));
entry->Remove();
}

View file

@ -0,0 +1,99 @@
#include "panda_qt/config_window.hpp"
ConfigWindow::ConfigWindow(QWidget* parent) : QDialog(parent) {
setWindowTitle(tr("Configuration"));
// Set up theme selection
setTheme(Theme::Dark);
themeSelect = new QComboBox(this);
themeSelect->addItem(tr("System"));
themeSelect->addItem(tr("Light"));
themeSelect->addItem(tr("Dark"));
themeSelect->addItem(tr("Greetings Cat"));
themeSelect->setCurrentIndex(static_cast<int>(currentTheme));
themeSelect->setGeometry(40, 40, 100, 50);
themeSelect->show();
connect(themeSelect, &QComboBox::currentIndexChanged, this, [&](int index) { setTheme(static_cast<Theme>(index)); });
}
void ConfigWindow::setTheme(Theme theme) {
currentTheme = theme;
switch (theme) {
case Theme::Dark: {
QApplication::setStyle(QStyleFactory::create("Fusion"));
QPalette p;
p.setColor(QPalette::Window, QColor(53, 53, 53));
p.setColor(QPalette::WindowText, Qt::white);
p.setColor(QPalette::Base, QColor(25, 25, 25));
p.setColor(QPalette::AlternateBase, QColor(53, 53, 53));
p.setColor(QPalette::ToolTipBase, Qt::white);
p.setColor(QPalette::ToolTipText, Qt::white);
p.setColor(QPalette::Text, Qt::white);
p.setColor(QPalette::Button, QColor(53, 53, 53));
p.setColor(QPalette::ButtonText, Qt::white);
p.setColor(QPalette::BrightText, Qt::red);
p.setColor(QPalette::Link, QColor(42, 130, 218));
p.setColor(QPalette::Highlight, QColor(42, 130, 218));
p.setColor(QPalette::HighlightedText, Qt::black);
qApp->setPalette(p);
break;
}
case Theme::Light: {
QApplication::setStyle(QStyleFactory::create("Fusion"));
QPalette p;
p.setColor(QPalette::Window, Qt::white);
p.setColor(QPalette::WindowText, Qt::black);
p.setColor(QPalette::Base, QColor(243, 243, 243));
p.setColor(QPalette::AlternateBase, Qt::white);
p.setColor(QPalette::ToolTipBase, Qt::black);
p.setColor(QPalette::ToolTipText, Qt::black);
p.setColor(QPalette::Text, Qt::black);
p.setColor(QPalette::Button, Qt::white);
p.setColor(QPalette::ButtonText, Qt::black);
p.setColor(QPalette::BrightText, Qt::red);
p.setColor(QPalette::Link, QColor(42, 130, 218));
p.setColor(QPalette::Highlight, QColor(42, 130, 218));
p.setColor(QPalette::HighlightedText, Qt::white);
qApp->setPalette(p);
break;
}
case Theme::GreetingsCat: {
QApplication::setStyle(QStyleFactory::create("Fusion"));
QPalette p;
p.setColor(QPalette::Window, QColor(250, 207, 228));
p.setColor(QPalette::WindowText, QColor(225, 22, 137));
p.setColor(QPalette::Base, QColor(250, 207, 228));
p.setColor(QPalette::AlternateBase, QColor(250, 207, 228));
p.setColor(QPalette::ToolTipBase, QColor(225, 22, 137));
p.setColor(QPalette::ToolTipText, QColor(225, 22, 137));
p.setColor(QPalette::Text, QColor(225, 22, 137));
p.setColor(QPalette::Button, QColor(250, 207, 228));
p.setColor(QPalette::ButtonText, QColor(225, 22, 137));
p.setColor(QPalette::BrightText, Qt::black);
p.setColor(QPalette::Link, QColor(42, 130, 218));
p.setColor(QPalette::Highlight, QColor(42, 130, 218));
p.setColor(QPalette::HighlightedText, Qt::black);
qApp->setPalette(p);
break;
}
case Theme::System: {
qApp->setPalette(this->style()->standardPalette());
qApp->setStyle(QStyleFactory::create("WindowsVista"));
qApp->setStyleSheet("");
break;
}
}
}
ConfigWindow::~ConfigWindow() { delete themeSelect; }

View file

@ -1,7 +1,13 @@
#include "panda_qt/main_window.hpp"
#include <QDesktopServices>
#include <QFileDialog>
#include <QString>
#include <cmath>
#include <cstdio>
#include <fstream>
#include "cheats.hpp"
MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent), screen(this) {
setWindowTitle("Alber");
@ -20,32 +26,58 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent)
auto fileMenu = menuBar->addMenu(tr("File"));
auto emulationMenu = menuBar->addMenu(tr("Emulation"));
auto toolsMenu = menuBar->addMenu(tr("Tools"));
auto helpMenu = menuBar->addMenu(tr("Help"));
auto aboutMenu = menuBar->addMenu(tr("About"));
// Create and bind actions for them
auto pandaAction = fileMenu->addAction(tr("panda..."));
connect(pandaAction, &QAction::triggered, this, &MainWindow::selectROM);
auto loadGameAction = fileMenu->addAction(tr("Load game"));
auto loadLuaAction = fileMenu->addAction(tr("Load Lua script"));
auto openAppFolderAction = fileMenu->addAction(tr("Open Panda3DS folder"));
connect(loadGameAction, &QAction::triggered, this, &MainWindow::selectROM);
connect(loadLuaAction, &QAction::triggered, this, &MainWindow::selectLuaFile);
connect(openAppFolderAction, &QAction::triggered, this, [this]() {
QString path = QString::fromStdU16String(emu->getAppDataRoot().u16string());
QDesktopServices::openUrl(QUrl::fromLocalFile(path));
});
auto pauseAction = emulationMenu->addAction(tr("Pause"));
auto resumeAction = emulationMenu->addAction(tr("Resume"));
auto resetAction = emulationMenu->addAction(tr("Reset"));
auto configureAction = emulationMenu->addAction(tr("Configure"));
connect(pauseAction, &QAction::triggered, this, [this]() { sendMessage(EmulatorMessage{.type = MessageType::Pause}); });
connect(resumeAction, &QAction::triggered, this, [this]() { sendMessage(EmulatorMessage{.type = MessageType::Resume}); });
connect(resetAction, &QAction::triggered, this, [this]() { sendMessage(EmulatorMessage{.type = MessageType::Reset}); });
connect(configureAction, &QAction::triggered, this, [this]() { configWindow->show(); });
auto dumpRomFSAction = toolsMenu->addAction(tr("Dump RomFS"));
auto luaEditorAction = toolsMenu->addAction(tr("Open Lua Editor"));
auto cheatsEditorAction = toolsMenu->addAction(tr("Open Cheats Editor"));
connect(dumpRomFSAction, &QAction::triggered, this, &MainWindow::dumpRomFS);
connect(luaEditorAction, &QAction::triggered, this, &MainWindow::openLuaEditor);
connect(cheatsEditorAction, &QAction::triggered, this, &MainWindow::openCheatsEditor);
// Set up theme selection
setTheme(Theme::Dark);
themeSelect = new QComboBox(this);
themeSelect->addItem(tr("System"));
themeSelect->addItem(tr("Light"));
themeSelect->addItem(tr("Dark"));
themeSelect->setCurrentIndex(static_cast<int>(currentTheme));
themeSelect->setGeometry(40, 40, 100, 50);
themeSelect->show();
connect(themeSelect, &QComboBox::currentIndexChanged, this, [&](int index) { setTheme(static_cast<Theme>(index)); });
auto aboutAction = aboutMenu->addAction(tr("About Panda3DS"));
connect(aboutAction, &QAction::triggered, this, &MainWindow::showAboutMenu);
emu = new Emulator();
emu->setOutputSize(screen.surfaceWidth, screen.surfaceHeight);
// The emulator graphics context for the thread should be initialized in the emulator thread due to how GL contexts work
// Set up misc objects
aboutWindow = new AboutWindow(nullptr);
configWindow = new ConfigWindow(this);
cheatsEditor = new CheatsWindow(emu, {}, this);
luaEditor = new TextEditorWindow(this, "script.lua", "");
auto args = QCoreApplication::arguments();
if (args.size() > 1) {
auto romPath = std::filesystem::current_path() / args.at(1).toStdU16String();
if (!emu->loadROM(romPath)) {
// For some reason just .c_str() doesn't show the proper path
Helpers::warn("Failed to load ROM file: %s", romPath.string().c_str());
}
}
// The emulator graphics context for the thread should be initialized in the emulator thread due to how GL contexts work
emuThread = std::thread([this]() {
const RendererType rendererType = emu->getConfig().rendererType;
usingGL = (rendererType == RendererType::OpenGL || rendererType == RendererType::Software || rendererType == RendererType::Null);
@ -73,17 +105,21 @@ void MainWindow::emuThreadMainLoop() {
{
std::unique_lock lock(messageQueueMutex);
if (needToLoadROM) {
needToLoadROM = false;
bool success = emu->loadROM(romToLoad);
if (!success) {
printf("Failed to load ROM");
// Dispatch all messages in the message queue
if (!messageQueue.empty()) {
for (const auto& msg : messageQueue) {
dispatchMessage(msg);
}
messageQueue.clear();
}
}
emu->runFrame();
if (emu->romType != ROMType::None) {
emu->getServiceManager().getHID().updateInputs(emu->getTicks());
}
swapEmuBuffer();
}
@ -102,94 +138,66 @@ void MainWindow::swapEmuBuffer() {
}
void MainWindow::selectROM() {
// Are we already waiting for a ROM to be loaded? Then complain about it!
{
std::unique_lock lock(messageQueueMutex);
if (needToLoadROM) {
QMessageBox::warning(this, tr("Already loading ROM"), tr("Panda3DS is already busy loading a ROM, please wait"));
return;
}
}
auto path =
QFileDialog::getOpenFileName(this, tr("Select 3DS ROM to load"), "", tr("Nintendo 3DS ROMs (*.3ds *.cci *.cxi *.app *.3dsx *.elf *.axf)"));
if (!path.isEmpty()) {
std::unique_lock lock(messageQueueMutex);
std::filesystem::path* p = new std::filesystem::path(path.toStdU16String());
romToLoad = path.toStdU16String();
needToLoadROM = true;
EmulatorMessage message{.type = MessageType::LoadROM};
message.path.p = p;
sendMessage(message);
}
}
void MainWindow::selectLuaFile() {
auto path = QFileDialog::getOpenFileName(this, tr("Select Lua script to load"), "", tr("Lua scripts (*.lua *.txt)"));
if (!path.isEmpty()) {
std::ifstream file(std::filesystem::path(path.toStdU16String()), std::ios::in);
if (file.fail()) {
printf("Failed to load selected lua file\n");
return;
}
// Read whole file into an std::string string
// Get file size, preallocate std::string to avoid furthermemory allocations
std::string code;
file.seekg(0, std::ios::end);
code.resize(file.tellg());
// Rewind and read the whole file
file.seekg(0, std::ios::beg);
file.read(&code[0], code.size());
file.close();
loadLuaScript(code);
// Copy the Lua script to the Lua editor
luaEditor->setText(code);
}
}
// Cleanup when the main window closes
MainWindow::~MainWindow() {
appRunning = false; // Set our running atomic to false in order to make the emulator thread stop, and join it
appRunning = false; // Set our running atomic to false in order to make the emulator thread stop, and join it
if (emuThread.joinable()) {
emuThread.join();
}
delete emu;
delete menuBar;
delete themeSelect;
delete aboutWindow;
delete configWindow;
delete cheatsEditor;
delete luaEditor;
}
void MainWindow::setTheme(Theme theme) {
currentTheme = theme;
switch (theme) {
case Theme::Dark: {
QApplication::setStyle(QStyleFactory::create("Fusion"));
QPalette p;
p.setColor(QPalette::Window, QColor(53, 53, 53));
p.setColor(QPalette::WindowText, Qt::white);
p.setColor(QPalette::Base, QColor(25, 25, 25));
p.setColor(QPalette::AlternateBase, QColor(53, 53, 53));
p.setColor(QPalette::ToolTipBase, Qt::white);
p.setColor(QPalette::ToolTipText, Qt::white);
p.setColor(QPalette::Text, Qt::white);
p.setColor(QPalette::Button, QColor(53, 53, 53));
p.setColor(QPalette::ButtonText, Qt::white);
p.setColor(QPalette::BrightText, Qt::red);
p.setColor(QPalette::Link, QColor(42, 130, 218));
p.setColor(QPalette::Highlight, QColor(42, 130, 218));
p.setColor(QPalette::HighlightedText, Qt::black);
qApp->setPalette(p);
break;
}
case Theme::Light: {
QApplication::setStyle(QStyleFactory::create("Fusion"));
QPalette p;
p.setColor(QPalette::Window, Qt::white);
p.setColor(QPalette::WindowText, Qt::black);
p.setColor(QPalette::Base, QColor(243, 243, 243));
p.setColor(QPalette::AlternateBase, Qt::white);
p.setColor(QPalette::ToolTipBase, Qt::black);
p.setColor(QPalette::ToolTipText, Qt::black);
p.setColor(QPalette::Text, Qt::black);
p.setColor(QPalette::Button, Qt::white);
p.setColor(QPalette::ButtonText, Qt::black);
p.setColor(QPalette::BrightText, Qt::red);
p.setColor(QPalette::Link, QColor(42, 130, 218));
p.setColor(QPalette::Highlight, QColor(42, 130, 218));
p.setColor(QPalette::HighlightedText, Qt::white);
qApp->setPalette(p);
break;
}
case Theme::System: {
qApp->setPalette(this->style()->standardPalette());
qApp->setStyle(QStyleFactory::create("WindowsVista"));
qApp->setStyleSheet("");
break;
}
}
// Send a message to the emulator thread. Lock the mutex and just push back to the vector.
void MainWindow::sendMessage(const EmulatorMessage& message) {
std::unique_lock lock(messageQueueMutex);
messageQueue.push_back(message);
}
void MainWindow::dumpRomFS() {
@ -201,14 +209,13 @@ void MainWindow::dumpRomFS() {
return;
}
std::filesystem::path path(folder.toStdU16String());
// TODO: This might break if the game accesses RomFS while we're dumping, we should move it to the emulator thread when we've got a message queue going
messageQueueMutex.lock();
RomFS::DumpingResult res = emu->dumpRomFS(path);
messageQueueMutex.unlock();
switch (res) {
case RomFS::DumpingResult::Success: break; // Yay!
case RomFS::DumpingResult::Success: break; // Yay!
case RomFS::DumpingResult::InvalidFormat: {
QMessageBox messageBox(
QMessageBox::Icon::Warning, tr("Invalid format for RomFS dumping"),
@ -225,4 +232,186 @@ void MainWindow::dumpRomFS() {
QMessageBox::warning(this, tr("No RomFS found"), tr("No RomFS partition was found in the loaded app"));
break;
}
}
void MainWindow::showAboutMenu() {
AboutWindow about(this);
about.exec();
}
void MainWindow::openLuaEditor() { luaEditor->show(); }
void MainWindow::openCheatsEditor() { cheatsEditor->show(); }
void MainWindow::dispatchMessage(const EmulatorMessage& message) {
switch (message.type) {
case MessageType::LoadROM:
emu->loadROM(*message.path.p);
// Clean up the allocated path
delete message.path.p;
break;
case MessageType::LoadLuaScript:
emu->getLua().loadString(*message.string.str);
delete message.string.str;
break;
case MessageType::EditCheat: {
u32 handle = message.cheat.c->handle;
const std::vector<uint8_t>& cheat = message.cheat.c->cheat;
const std::function<void(u32)>& callback = message.cheat.c->callback;
bool isEditing = handle != Cheats::badCheatHandle;
if (isEditing) {
emu->getCheats().removeCheat(handle);
u32 handle = emu->getCheats().addCheat(cheat.data(), cheat.size());
} else {
u32 handle = emu->getCheats().addCheat(cheat.data(), cheat.size());
callback(handle);
}
delete message.cheat.c;
} break;
case MessageType::Pause: emu->pause(); break;
case MessageType::Resume: emu->resume(); break;
case MessageType::TogglePause: emu->togglePause(); break;
case MessageType::Reset: emu->reset(Emulator::ReloadOption::Reload); break;
case MessageType::PressKey: emu->getServiceManager().getHID().pressKey(message.key.key); break;
case MessageType::ReleaseKey: emu->getServiceManager().getHID().releaseKey(message.key.key); break;
case MessageType::SetCirclePadX: emu->getServiceManager().getHID().setCirclepadX(message.circlepad.value); break;
case MessageType::SetCirclePadY: emu->getServiceManager().getHID().setCirclepadY(message.circlepad.value); break;
case MessageType::PressTouchscreen:
emu->getServiceManager().getHID().setTouchScreenPress(message.touchscreen.x, message.touchscreen.y);
break;
case MessageType::ReleaseTouchscreen: emu->getServiceManager().getHID().releaseTouchScreen(); break;
}
}
void MainWindow::keyPressEvent(QKeyEvent* event) {
auto pressKey = [this](u32 key) {
EmulatorMessage message{.type = MessageType::PressKey};
message.key.key = key;
sendMessage(message);
};
auto setCirclePad = [this](MessageType type, s16 value) {
EmulatorMessage message{.type = type};
message.circlepad.value = value;
sendMessage(message);
};
switch (event->key()) {
case Qt::Key_L: pressKey(HID::Keys::A); break;
case Qt::Key_K: pressKey(HID::Keys::B); break;
case Qt::Key_O: pressKey(HID::Keys::X); break;
case Qt::Key_I: pressKey(HID::Keys::Y); break;
case Qt::Key_Q: pressKey(HID::Keys::L); break;
case Qt::Key_P: pressKey(HID::Keys::R); break;
case Qt::Key_W: setCirclePad(MessageType::SetCirclePadY, 0x9C); break;
case Qt::Key_A: setCirclePad(MessageType::SetCirclePadX, -0x9C); break;
case Qt::Key_S: setCirclePad(MessageType::SetCirclePadY, -0x9C); break;
case Qt::Key_D: setCirclePad(MessageType::SetCirclePadX, 0x9C); break;
case Qt::Key_Right: pressKey(HID::Keys::Right); break;
case Qt::Key_Left: pressKey(HID::Keys::Left); break;
case Qt::Key_Up: pressKey(HID::Keys::Up); break;
case Qt::Key_Down: pressKey(HID::Keys::Down); break;
case Qt::Key_Return: pressKey(HID::Keys::Start); break;
case Qt::Key_Backspace: pressKey(HID::Keys::Select); break;
case Qt::Key_F4: sendMessage(EmulatorMessage{.type = MessageType::TogglePause}); break;
case Qt::Key_F5: sendMessage(EmulatorMessage{.type = MessageType::Reset}); break;
}
}
void MainWindow::keyReleaseEvent(QKeyEvent* event) {
auto releaseKey = [this](u32 key) {
EmulatorMessage message{.type = MessageType::ReleaseKey};
message.key.key = key;
sendMessage(message);
};
auto releaseCirclePad = [this](MessageType type) {
EmulatorMessage message{.type = type};
message.circlepad.value = 0;
sendMessage(message);
};
switch (event->key()) {
case Qt::Key_L: releaseKey(HID::Keys::A); break;
case Qt::Key_K: releaseKey(HID::Keys::B); break;
case Qt::Key_O: releaseKey(HID::Keys::X); break;
case Qt::Key_I: releaseKey(HID::Keys::Y); break;
case Qt::Key_Q: releaseKey(HID::Keys::L); break;
case Qt::Key_P: releaseKey(HID::Keys::R); break;
case Qt::Key_W:
case Qt::Key_S: releaseCirclePad(MessageType::SetCirclePadY); break;
case Qt::Key_A:
case Qt::Key_D: releaseCirclePad(MessageType::SetCirclePadX); break;
case Qt::Key_Right: releaseKey(HID::Keys::Right); break;
case Qt::Key_Left: releaseKey(HID::Keys::Left); break;
case Qt::Key_Up: releaseKey(HID::Keys::Up); break;
case Qt::Key_Down: releaseKey(HID::Keys::Down); break;
case Qt::Key_Return: releaseKey(HID::Keys::Start); break;
case Qt::Key_Backspace: releaseKey(HID::Keys::Select); break;
}
}
void MainWindow::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::MouseButton::LeftButton) {
const QPointF clickPos = event->globalPosition();
const QPointF widgetPos = screen.mapFromGlobal(clickPos);
// Press is inside the screen area
if (widgetPos.x() >= 0 && widgetPos.x() < screen.width() && widgetPos.y() >= 0 && widgetPos.y() < screen.height()) {
// Go from widget positions to [0, 400) for x and [0, 480) for y
uint x = (uint)std::round(widgetPos.x() / screen.width() * 400.f);
uint y = (uint)std::round(widgetPos.y() / screen.height() * 480.f);
// Check if touch falls in the touch screen area
if (y >= 240 && y <= 480 && x >= 40 && x < 40 + 320) {
// Convert to 3DS coordinates
u16 x_converted = static_cast<u16>(x) - 40;
u16 y_converted = static_cast<u16>(y) - 240;
EmulatorMessage message{.type = MessageType::PressTouchscreen};
message.touchscreen.x = x_converted;
message.touchscreen.y = y_converted;
sendMessage(message);
} else {
sendMessage(EmulatorMessage{.type = MessageType::ReleaseTouchscreen});
}
}
}
}
void MainWindow::mouseReleaseEvent(QMouseEvent* event) {
if (event->button() == Qt::MouseButton::LeftButton) {
sendMessage(EmulatorMessage{.type = MessageType::ReleaseTouchscreen});
}
}
void MainWindow::loadLuaScript(const std::string& code) {
EmulatorMessage message{.type = MessageType::LoadLuaScript};
// Make a copy of the code on the heap to send via the message queue
message.string.str = new std::string(code);
sendMessage(message);
}
void MainWindow::editCheat(u32 handle, const std::vector<uint8_t>& cheat, const std::function<void(u32)>& callback) {
EmulatorMessage message{.type = MessageType::EditCheat};
CheatMessage* c = new CheatMessage();
c->handle = handle;
c->cheat = cheat;
c->callback = callback;
message.cheat.c = c;
sendMessage(message);
}

View file

@ -0,0 +1,44 @@
#include "panda_qt/text_editor.hpp"
#include <QPushButton>
#include <QVBoxLayout>
#include "panda_qt/main_window.hpp"
using namespace Zep;
TextEditorWindow::TextEditorWindow(QWidget* parent, const std::string& filename, const std::string& initialText)
: QDialog(parent), zepWidget(this, qApp->applicationDirPath().toStdString(), fontSize) {
resize(600, 600);
// Register our extensions
ZepRegressExCommand::Register(zepWidget.GetEditor());
ZepReplExCommand::Register(zepWidget.GetEditor(), &replProvider);
// Default to standard mode instead of vim mode, initialize text box
zepWidget.GetEditor().InitWithText(filename, initialText);
zepWidget.GetEditor().SetGlobalMode(Zep::ZepMode_Standard::StaticName());
// Layout for widgets
QVBoxLayout* mainLayout = new QVBoxLayout();
setLayout(mainLayout);
QPushButton* button = new QPushButton(tr("Load script"), this);
button->setFixedSize(100, 20);
// When the Load Script button is pressed, send the current text to the MainWindow, which will upload it to the emulator's lua object
connect(button, &QPushButton::pressed, this, [this]() {
if (parentWidget()) {
auto buffer = zepWidget.GetEditor().GetMRUBuffer();
const std::string text = buffer->GetBufferText(buffer->Begin(), buffer->End());
static_cast<MainWindow*>(parentWidget())->loadLuaScript(text);
} else {
// This should be unreachable, only here for safety purposes
printf("Text editor does not have any parent widget, click doesn't work :(\n");
}
});
mainLayout->addWidget(button);
mainLayout->addWidget(&zepWidget);
}

2
src/panda_qt/zep.cpp Normal file
View file

@ -0,0 +1,2 @@
#define ZEP_SINGLE_HEADER_BUILD
#include "zep.h"

15
src/pandroid/.gitignore vendored Normal file
View file

@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

View file

@ -0,0 +1,56 @@
plugins {
id("com.android.application")
}
android {
namespace = "com.panda3ds.pandroid"
compileSdk = 33
defaultConfig {
applicationId = "com.panda3ds.pandroid"
minSdk = 24
targetSdk = 33
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ndk {
abiFilters += listOf("x86_64", "arm64-v8a")
}
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
isShrinkResources = false
isDebuggable = false
signingConfig = signingConfigs.getByName("debug")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
getByName("debug") {
isMinifyEnabled = false
isShrinkResources = false
isDebuggable = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
dependencies {
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.8.0")
implementation("androidx.preference:preference:1.2.1")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("com.google.code.gson:gson:2.10.1")
}

21
src/pandroid/app/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
<uses-feature
android:required="true"
android:glEsVersion="0x0030001"/>
<application
android:name=".app.PandroidApplication"
android:requestLegacyExternalStorage="true"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:isGame="true"
android:hardwareAccelerated="true"
android:theme="@style/Theme.Pandroid"
tools:targetApi="31">
<activity
android:name=".app.MainActivity"
android:exported="true"
android:configChanges="orientation">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".app.GameActivity"
android:configChanges="screenSize|screenLayout|orientation|density|uiMode">
</activity>
<activity
android:name=".app.editor.CodeEditorActivity"
android:windowSoftInputMode="adjustResize"
android:configChanges="screenSize|screenLayout|orientation|density|uiMode">
</activity>
<activity android:name=".app.PreferenceActivity"
android:launchMode="standard"
android:configChanges="screenSize|screenLayout|orientation|density"/>
<activity android:name=".app.preferences.InputMapActivity"
android:configChanges="density|orientation|screenSize"/>
<service android:name=".app.services.LoggerService" android:process=":logger_service"/>
</application>
</manifest>

Binary file not shown.

View file

@ -0,0 +1,28 @@
package com.panda3ds.pandroid;
import android.util.Log;
public class AlberDriver {
AlberDriver() { super(); }
public static native void Setup();
public static native void Initialize();
public static native void RunFrame(int fbo);
public static native boolean HasRomLoaded();
public static native void LoadRom(String path);
public static native void Finalize();
public static native void KeyDown(int code);
public static native void KeyUp(int code);
public static native void SetCirclepadAxis(int x, int y);
public static native void TouchScreenUp();
public static native void TouchScreenDown(int x, int y);
public static native void Pause();
public static native void Resume();
public static native void LoadLuaScript(String script);
public static native byte[] GetSmdh();
public static native void setShaderJitEnabled(boolean enable);
static { System.loadLibrary("Alber"); }
}

View file

@ -0,0 +1,32 @@
package com.panda3ds.pandroid.app;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.data.config.GlobalConfig;
public class BaseActivity extends AppCompatActivity {
private int currentTheme = PandroidApplication.getThemeId();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
applyTheme();
super.onCreate(savedInstanceState);
}
@Override
protected void onResume() {
super.onResume();
if (PandroidApplication.getThemeId() != currentTheme) {
recreate();
}
}
private void applyTheme() {
currentTheme = PandroidApplication.getThemeId();
setTheme(currentTheme);
}
}

View file

@ -0,0 +1,120 @@
package com.panda3ds.pandroid.app;
import android.content.Intent;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.CheckBox;
import android.widget.FrameLayout;
import android.widget.Toast;
import androidx.annotation.Nullable;
import com.panda3ds.pandroid.AlberDriver;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.app.game.AlberInputListener;
import com.panda3ds.pandroid.app.game.DrawerFragment;
import com.panda3ds.pandroid.data.config.GlobalConfig;
import com.panda3ds.pandroid.input.InputHandler;
import com.panda3ds.pandroid.input.InputMap;
import com.panda3ds.pandroid.utils.Constants;
import com.panda3ds.pandroid.view.PandaGlSurfaceView;
import com.panda3ds.pandroid.view.PandaLayoutController;
import com.panda3ds.pandroid.view.utils.PerformanceView;
public class GameActivity extends BaseActivity {
private final DrawerFragment drawerFragment = new DrawerFragment();
private final AlberInputListener inputListener = new AlberInputListener(this::onBackPressed);
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
if (!intent.hasExtra(Constants.ACTIVITY_PARAMETER_PATH)) {
setContentView(new FrameLayout(this));
Toast.makeText(this, "Invalid rom path!", Toast.LENGTH_LONG).show();
finish();
return;
}
PandaGlSurfaceView pandaSurface = new PandaGlSurfaceView(this, intent.getStringExtra(Constants.ACTIVITY_PARAMETER_PATH));
setContentView(R.layout.game_activity);
((FrameLayout) findViewById(R.id.panda_gl_frame))
.addView(pandaSurface, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
PandaLayoutController controllerLayout = findViewById(R.id.controller_layout);
controllerLayout.initialize();
((CheckBox) findViewById(R.id.hide_screen_controller)).setOnCheckedChangeListener((buttonView, checked) -> {
findViewById(R.id.overlay_controller).setVisibility(checked ? View.VISIBLE : View.GONE);
findViewById(R.id.overlay_controller).invalidate();
findViewById(R.id.overlay_controller).requestLayout();
GlobalConfig.set(GlobalConfig.KEY_SCREEN_GAMEPAD_VISIBLE, checked);
});
((CheckBox) findViewById(R.id.hide_screen_controller)).setChecked(GlobalConfig.get(GlobalConfig.KEY_SCREEN_GAMEPAD_VISIBLE));
getSupportFragmentManager().beginTransaction().replace(R.id.drawer_fragment, drawerFragment).commitNow();
if (GlobalConfig.get(GlobalConfig.KEY_SHOW_PERFORMANCE_OVERLAY)) {
PerformanceView view = new PerformanceView(this);
((FrameLayout) findViewById(R.id.panda_gl_frame)).addView(view, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
}
}
@Override
protected void onResume() {
super.onResume();
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
InputHandler.reset();
InputHandler.setMotionDeadZone(InputMap.getDeadZone());
InputHandler.setEventListener(inputListener);
}
@Override
protected void onPause() {
super.onPause();
InputHandler.reset();
drawerFragment.open();
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if ((!drawerFragment.isOpened()) && InputHandler.processKeyEvent(event)) {
return true;
}
return super.dispatchKeyEvent(event);
}
@Override
public void onBackPressed() {
if (drawerFragment.isOpened()) {
drawerFragment.close();
} else {
drawerFragment.open();
}
}
@Override
public boolean dispatchGenericMotionEvent(MotionEvent ev) {
if ((!drawerFragment.isOpened()) && InputHandler.processMotionEvent(ev)) {
return true;
}
return super.dispatchGenericMotionEvent(ev);
}
@Override
protected void onDestroy() {
if (AlberDriver.HasRomLoaded()) {
AlberDriver.Finalize();
}
super.onDestroy();
}
}

View file

@ -0,0 +1,72 @@
package com.panda3ds.pandroid.app;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static android.provider.Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.view.MenuItem;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import com.google.android.material.navigation.NavigationBarView;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.app.editor.CodeEditorActivity;
import com.panda3ds.pandroid.app.main.GamesFragment;
import com.panda3ds.pandroid.app.main.SearchFragment;
import com.panda3ds.pandroid.app.main.SettingsFragment;
import java.io.File;
public class MainActivity extends BaseActivity implements NavigationBarView.OnItemSelectedListener {
private static final int PICK_ROM = 2;
private static final int PERMISSION_REQUEST_CODE = 3;
private final GamesFragment gamesFragment = new GamesFragment();
private final SearchFragment searchFragment = new SearchFragment();
private final SettingsFragment settingsFragment = new SettingsFragment();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (!Environment.isExternalStorageManager()) {
Intent intent = new Intent(ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
startActivity(intent);
}
} else {
ActivityCompat.requestPermissions(this, new String[] {READ_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE);
ActivityCompat.requestPermissions(this, new String[] {WRITE_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE);
}
setContentView(R.layout.activity_main);
NavigationBarView bar = findViewById(R.id.navigation);
bar.setOnItemSelectedListener(this);
bar.postDelayed(() -> bar.setSelectedItemId(bar.getSelectedItemId()), 5);
}
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
int id = item.getItemId();
FragmentManager manager = getSupportFragmentManager();
Fragment fragment;
if (id == R.id.games) {
fragment = gamesFragment;
} else if (id == R.id.search) {
fragment = searchFragment;
} else if (id == R.id.settings) {
fragment = settingsFragment;
} else {
return false;
}
manager.beginTransaction().replace(R.id.fragment_container, fragment).commitNow();
return true;
}
}

View file

@ -0,0 +1,63 @@
package com.panda3ds.pandroid.app;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.Resources;
import com.panda3ds.pandroid.AlberDriver;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.app.services.LoggerService;
import com.panda3ds.pandroid.data.config.GlobalConfig;
import com.panda3ds.pandroid.input.InputMap;
import com.panda3ds.pandroid.utils.GameUtils;
public class PandroidApplication extends Application {
private static Context appContext;
@Override
public void onCreate() {
super.onCreate();
appContext = this;
GlobalConfig.initialize();
GameUtils.initialize();
InputMap.initialize();
AlberDriver.Setup();
if (GlobalConfig.get(GlobalConfig.KEY_LOGGER_SERVICE)) {
startService(new Intent(this, LoggerService.class));
}
}
public static int getThemeId() {
switch (GlobalConfig.get(GlobalConfig.KEY_APP_THEME)) {
case GlobalConfig.THEME_LIGHT:
return R.style.Theme_Pandroid_Light;
case GlobalConfig.THEME_DARK:
return R.style.Theme_Pandroid_Dark;
case GlobalConfig.THEME_BLACK:
return R.style.Theme_Pandroid_Black;
}
return R.style.Theme_Pandroid;
}
public static boolean isDarkMode() {
switch (GlobalConfig.get(GlobalConfig.KEY_APP_THEME)) {
case GlobalConfig.THEME_DARK:
case GlobalConfig.THEME_BLACK:
return true;
case GlobalConfig.THEME_LIGHT:
return false;
}
Resources res = Resources.getSystem();
int nightFlags = res.getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
return nightFlags == Configuration.UI_MODE_NIGHT_YES;
}
public static Context getAppContext() { return appContext; }
}

View file

@ -0,0 +1,59 @@
package com.panda3ds.pandroid.app;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.MenuItem;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.utils.Constants;
public class PreferenceActivity extends BaseActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
setContentView(R.layout.activity_preference);
setSupportActionBar(findViewById(R.id.toolbar));
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
if (!intent.hasExtra(Constants.ACTIVITY_PARAMETER_FRAGMENT)) {
finish();
return;
}
try {
Class<?> clazz = getClassLoader().loadClass(intent.getStringExtra(Constants.ACTIVITY_PARAMETER_FRAGMENT));
Fragment fragment = (Fragment) clazz.newInstance();
fragment.setArguments(intent.getExtras());
getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, fragment).commitNow();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void launch(Context context, Class<? extends Fragment> clazz) {
launch(context, clazz, new Intent());
}
public static void launch(Context context, Class<? extends Fragment> clazz, Intent extras) {
context.startActivity(new Intent(context, PreferenceActivity.class)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtras(extras)
.putExtra(Constants.ACTIVITY_PARAMETER_FRAGMENT, clazz.getName()));
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
}
return super.onOptionsItemSelected(item);
}
}

View file

@ -0,0 +1,27 @@
package com.panda3ds.pandroid.app.base;
import android.annotation.SuppressLint;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AppCompatActivity;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.SwitchPreference;
import com.panda3ds.pandroid.lang.Function;
public abstract class BasePreferenceFragment extends PreferenceFragmentCompat {
@SuppressLint("RestrictedApi")
protected void setItemClick(String key, Function<Preference> listener) {
findPreference(key).setOnPreferenceClickListener(preference -> {
listener.run(preference);
getPreferenceScreen().performClick();
return false;
});
}
protected void setActivityTitle(@StringRes int titleId) {
((AppCompatActivity) requireActivity()).getSupportActionBar().setTitle(titleId);
}
}

View file

@ -0,0 +1,57 @@
package com.panda3ds.pandroid.app.base;
import android.content.Context;
import android.view.Gravity;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.AppCompatEditText;
import androidx.appcompat.widget.LinearLayoutCompat;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.lang.Function;
public class BottomAlertDialog extends AlertDialog.Builder {
private final LinearLayoutCompat layoutCompat;
public BottomAlertDialog(@NonNull Context context) {
super(context, R.style.AlertDialog);
layoutCompat = new LinearLayoutCompat(context);
layoutCompat.setOrientation(LinearLayoutCompat.VERTICAL);
int padding = getContext().getResources().getDimensionPixelSize(androidx.appcompat.R.dimen.abc_dialog_padding_material);
layoutCompat.setPadding(padding, 0, padding, 0);
setView(layoutCompat);
}
@NonNull
@Override
public AlertDialog create() {
AlertDialog dialog = super.create();
dialog.getWindow().setGravity(Gravity.BOTTOM | Gravity.CENTER);
dialog.getWindow().getAttributes().y = Math.round(getContext().getResources().getDisplayMetrics().density * 15);
return dialog;
}
public BottomAlertDialog setTextInput(String hint, Function<String> listener) {
AppCompatEditText edit = new AppCompatEditText(getContext());
edit.setHint(hint);
int margin = layoutCompat.getPaddingLeft() / 2;
LinearLayoutCompat.LayoutParams params = new LinearLayoutCompat.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.setMargins(0, margin, 0, margin);
layoutCompat.addView(edit, params);
setPositiveButton(android.R.string.ok, (dialog, which) -> listener.run(String.valueOf(edit.getText())));
setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss());
return this;
}
@Override
public AlertDialog show() {
AlertDialog dialog = create();
dialog.show();
return dialog;
}
}

View file

@ -0,0 +1,28 @@
package com.panda3ds.pandroid.app.base;
import android.app.Dialog;
import android.os.Bundle;
import android.view.Gravity;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import com.panda3ds.pandroid.R;
public class BottomDialogFragment extends DialogFragment {
@Override
public int getTheme() {
return R.style.AlertDialog;
}
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
Dialog dialog = super.onCreateDialog(savedInstanceState);
dialog.getWindow().setGravity(Gravity.CENTER | Gravity.BOTTOM);
dialog.getWindow().getAttributes().y = Math.round(getContext().getResources().getDisplayMetrics().density * 15);
return dialog;
}
}

View file

@ -0,0 +1,196 @@
package com.panda3ds.pandroid.app.editor;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Rect;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import androidx.activity.result.contract.ActivityResultContract;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatTextView;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.app.BaseActivity;
import com.panda3ds.pandroid.app.base.BottomAlertDialog;
import com.panda3ds.pandroid.lang.Task;
import com.panda3ds.pandroid.utils.FileUtils;
import com.panda3ds.pandroid.view.code.CodeEditor;
import com.panda3ds.pandroid.view.code.syntax.CodeSyntax;
import java.io.Serializable;
public class CodeEditorActivity extends BaseActivity {
private static final String TAB = " ";
private String path;
private String fileName;
private CodeEditor editor;
private AppCompatTextView title;
private View saveButton;
private boolean changed = false;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_code_editor);
Arguments args = (Arguments) getIntent().getSerializableExtra("args");
editor = findViewById(R.id.editor);
getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(this::onGlobalLayoutChanged);
path = args.path;
fileName = args.fileName;
title = findViewById(R.id.title);
title.setText(fileName);
saveButton = findViewById(R.id.save);
saveButton.setVisibility(View.GONE);
saveButton.setOnClickListener(v -> save());
new Task(() -> {
String content = FileUtils.readTextFile(path + "/" + fileName);
editor.post(() -> {
editor.setText(content);
editor.setSyntax(CodeSyntax.getFromFilename(fileName));
editor.setOnContentChangedListener(this::onDocumentContentChanged);
});
}).start();
switch (args.type) {
case LUA_SCRIPT_EDITOR:
setupLuaPatchEditor();
break;
case READ_ONLY_EDITOR:
setupReadOnlyEditor();
break;
}
onGlobalLayoutChanged();
findViewById(R.id.key_hide).setOnClickListener(v -> {
((InputMethodManager) getSystemService(INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(v.getWindowToken(), 0);
});
findViewById(R.id.key_tab).setOnClickListener(v -> {
editor.insert(TAB);
});
}
// Detect virtual keyboard is visible
private void onGlobalLayoutChanged() {
View view = getWindow().getDecorView();
Rect rect = new Rect();
view.getWindowVisibleDisplayFrame(rect);
int currentHeight = rect.height();
int height = view.getHeight();
if (currentHeight < height * 0.8) {
findViewById(R.id.keybar).setVisibility(View.VISIBLE);
} else {
findViewById(R.id.keybar).setVisibility(View.GONE);
}
}
private void setupReadOnlyEditor() {
editor.setEnabled(false);
editor.setFocusable(false);
}
private void setupLuaPatchEditor() {
findViewById(R.id.lua_toolbar).setVisibility(View.VISIBLE);
findViewById(R.id.lua_play).setOnClickListener(v -> {
if (changed) {
save();
}
setResult(Activity.RESULT_OK, new Intent(Result.ACTION_PLAY.name()));
finish();
});
}
@SuppressLint("SetTextI18n")
private void onDocumentContentChanged() {
changed = true;
title.setText("*" + fileName);
saveButton.setVisibility(View.VISIBLE);
}
public void save() {
title.setText(fileName);
saveButton.setVisibility(View.GONE);
changed = false;
new Task(() -> FileUtils.writeTextFile(path, fileName, String.valueOf(editor.getText()))).runSync();
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (event.getKeyCode() == KeyEvent.KEYCODE_TAB) {
if (event.getAction() == KeyEvent.ACTION_UP) {
editor.insert(TAB);
}
return true;
}
return super.dispatchKeyEvent(event);
}
@Override
public void onBackPressed() {
if (changed) {
new BottomAlertDialog(this)
.setNeutralButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss())
.setPositiveButton(R.string.save_and_exit, (dialog, which) -> {
save();
finish();
})
.setNegativeButton(R.string.exit_without_saving, (dialog, which) -> finish())
.setTitle(String.format(getString(R.string.exit_without_saving_title_ff), fileName)).show();
} else {
super.onBackPressed();
}
}
public static final class Arguments implements Serializable {
private final String path;
private final String fileName;
private final EditorType type;
public Arguments(String path, String fileName, EditorType type) {
this.path = path;
this.fileName = fileName;
this.type = type;
}
}
public enum Result {
ACTION_PLAY,
NULL
}
public enum EditorType {
LUA_SCRIPT_EDITOR,
READ_ONLY_EDITOR,
TEXT_EDITOR
}
public static final class Contract extends ActivityResultContract<Arguments, Result> {
@NonNull
@Override
public Intent createIntent(@NonNull Context context, Arguments args) {
return new Intent(context, CodeEditorActivity.class).putExtra("args", args);
}
@Override
public Result parseResult(int i, @Nullable Intent intent) {
return i == RESULT_OK && intent != null ? Result.valueOf(intent.getAction()) : Result.NULL;
}
}
}

View file

@ -0,0 +1,64 @@
package com.panda3ds.pandroid.app.game;
import com.panda3ds.pandroid.AlberDriver;
import com.panda3ds.pandroid.input.InputEvent;
import com.panda3ds.pandroid.input.InputMap;
import com.panda3ds.pandroid.input.KeyName;
import com.panda3ds.pandroid.lang.Function;
import com.panda3ds.pandroid.math.Vector2;
import java.util.Objects;
public class AlberInputListener implements Function<InputEvent> {
private final Runnable backListener;
public AlberInputListener(Runnable backListener) { this.backListener = backListener; }
private final Vector2 axis = new Vector2(0.0f, 0.0f);
@Override
public void run(InputEvent event) {
KeyName key = InputMap.relative(event.getName());
if (Objects.equals(event.getName(), "KEYCODE_BACK")) {
backListener.run();
return;
}
if (key == KeyName.NULL) {
return;
}
boolean axisChanged = false;
switch (key) {
case AXIS_UP:
axis.y = event.getValue();
axisChanged = true;
break;
case AXIS_DOWN:
axis.y = -event.getValue();
axisChanged = true;
break;
case AXIS_LEFT:
axis.x = -event.getValue();
axisChanged = true;
break;
case AXIS_RIGHT:
axis.x = event.getValue();
axisChanged = true;
break;
default:
if (event.isDown()) {
AlberDriver.KeyDown(key.getKeyId());
} else {
AlberDriver.KeyUp(key.getKeyId());
}
break;
}
if (axisChanged) {
AlberDriver.SetCirclepadAxis(Math.round(axis.x * 0x9C), Math.round(axis.y * 0x9C));
}
}
}

View file

@ -0,0 +1,117 @@
package com.panda3ds.pandroid.app.game;
import android.graphics.Color;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import com.google.android.material.navigation.NavigationView;
import com.panda3ds.pandroid.AlberDriver;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.data.game.GameMetadata;
import com.panda3ds.pandroid.utils.GameUtils;
import com.panda3ds.pandroid.view.gamesgrid.GameIconView;
public class DrawerFragment extends Fragment implements DrawerLayout.DrawerListener, NavigationView.OnNavigationItemSelectedListener {
private DrawerLayout drawerContainer;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
drawerContainer = requireActivity().findViewById(R.id.drawer_container);
drawerContainer.removeDrawerListener(this);
drawerContainer.addDrawerListener(this);
drawerContainer.setScrimColor(Color.argb(160, 0,0,0));
drawerContainer.setVisibility(View.GONE);
return inflater.inflate(R.layout.fragment_game_drawer, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
drawerContainer.setVisibility(View.GONE);
GameMetadata game = GameUtils.getCurrentGame();
((GameIconView)view.findViewById(R.id.game_icon)).setImageBitmap(game.getIcon());
((AppCompatTextView)view.findViewById(R.id.game_title)).setText(game.getTitle());
((AppCompatTextView)view.findViewById(R.id.game_publisher)).setText(game.getPublisher());
((NavigationView)view.findViewById(R.id.action_navigation)).setNavigationItemSelectedListener(this);
((NavigationView)view.findViewById(R.id.others_navigation)).setNavigationItemSelectedListener(this);
}
@Override
public void onDetach() {
if (drawerContainer != null) {
drawerContainer.removeDrawerListener(this);
}
super.onDetach();
}
private void refreshLayout() {
drawerContainer.measure(View.MeasureSpec.EXACTLY, View.MeasureSpec.EXACTLY);
drawerContainer.requestLayout();
drawerContainer.invalidate();
drawerContainer.forceLayout();
}
public void open() {
if (!drawerContainer.isOpen()) {
drawerContainer.setVisibility(View.VISIBLE);
drawerContainer.open();
drawerContainer.postDelayed(this::refreshLayout, 20);
}
}
public void close() {
if (drawerContainer.isOpen()) {
drawerContainer.close();
}
}
@Override
public void onDrawerSlide(@NonNull View drawerView, float slideOffset) {}
@Override
public void onDrawerOpened(@NonNull View drawerView) {
AlberDriver.Pause();
}
@Override
public void onDrawerClosed(@NonNull View drawerView) {
drawerContainer.setVisibility(View.GONE);
AlberDriver.Resume();
}
@Override
public void onDrawerStateChanged(int newState) {}
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
int id = item.getItemId();
if (id == R.id.resume) {
close();
} else if (id == R.id.exit) {
requireActivity().finish();
} else if (id == R.id.lua_script){
new LuaDialogFragment().show(getParentFragmentManager(), null);
}
return false;
}
public boolean isOpened() {
return drawerContainer.isOpen();
}
}

View file

@ -0,0 +1,185 @@
package com.panda3ds.pandroid.app.game;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.panda3ds.pandroid.AlberDriver;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.app.base.BottomAlertDialog;
import com.panda3ds.pandroid.app.base.BottomDialogFragment;
import com.panda3ds.pandroid.app.editor.CodeEditorActivity;
import com.panda3ds.pandroid.lang.Task;
import com.panda3ds.pandroid.utils.FileUtils;
import com.panda3ds.pandroid.view.recycler.AutoFitGridLayout;
import com.panda3ds.pandroid.view.recycler.SimpleListAdapter;
import java.util.ArrayList;
import java.util.UUID;
public class LuaDialogFragment extends BottomDialogFragment {
private final SimpleListAdapter<LuaFile> adapter = new SimpleListAdapter<>(R.layout.holder_lua_script, this::onCreateListItem);
private ActivityResultLauncher<CodeEditorActivity.Arguments> codeEditorLauncher;
private LuaFile currentEditorFile;
private ActivityResultLauncher<String[]> openDocumentLauncher;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.dialog_lua_scripts, container, false);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
openDocumentLauncher = registerForActivityResult(new ActivityResultContracts.OpenDocument(), result -> {
if (result != null) {
String fileName = FileUtils.getName(result.toString());
if (fileName.toLowerCase().endsWith(".lua")) {
new Task(() -> {
String content = FileUtils.readTextFile(result.toString());
createFile(FileUtils.getName(result.toString()), content);
}).start();
} else {
Toast.makeText(getContext(), R.string.file_not_supported, Toast.LENGTH_SHORT).show();
}
}
});
codeEditorLauncher = registerForActivityResult(new CodeEditorActivity.Contract(), result -> {
if (result != null) {
switch (result) {
case ACTION_PLAY:
loadScript(currentEditorFile);
break;
}
}
orderByModified();
});
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
view.findViewById(R.id.open_file).setOnClickListener(v -> {
openDocumentLauncher.launch(new String[]{"*/*"});
});
view.findViewById(R.id.create).setOnClickListener(v -> {
new BottomAlertDialog(requireContext())
.setTextInput(getString(R.string.name), arg -> {
String name = arg.trim();
if (name.length() > 1) {
new Task(() -> {
LuaFile file = createFile(name, "");
currentEditorFile = file;
codeEditorLauncher.launch(new CodeEditorActivity.Arguments(file.path, file.name, CodeEditorActivity.EditorType.LUA_SCRIPT_EDITOR));
}).start();
}
}).setTitle(R.string.create_new)
.show();
});
((RecyclerView) view.findViewById(R.id.recycler)).setAdapter(adapter);
((RecyclerView) view.findViewById(R.id.recycler)).setLayoutManager(new AutoFitGridLayout(getContext(), 140));
FileUtils.createDir(FileUtils.getResourcesPath(), "Lua Scripts");
ArrayList<LuaFile> files = new ArrayList<>();
String path = FileUtils.getResourcesPath() + "/Lua Scripts/";
for (String file : FileUtils.listFiles(path)) {
files.add(new LuaFile(file));
}
adapter.addAll(files);
orderByModified();
}
private LuaFile createFile(String name, String content) {
if (name.toLowerCase().endsWith(".lua")) {
name = name.substring(0, name.length() - 4);
}
name = name.replaceAll("[^[a-zA-Z0-9-_ ]]", "-");
String fileName = name + "." + UUID.randomUUID().toString().substring(0, 4) + ".lua";
LuaFile file = new LuaFile(fileName);
FileUtils.writeTextFile(file.path, fileName, content);
getView().post(() -> {
adapter.addAll(file);
orderByModified();
});
return file;
}
private void orderByModified() {
adapter.sort((o1, o2) -> Long.compare(o2.lastModified(), o1.lastModified()));
}
private void onCreateListItem(int position, LuaFile file, View view) {
((TextView) view.findViewById(R.id.title))
.setText(file.name.split("\\.")[0]);
view.setOnClickListener(v -> loadScript(file));
view.findViewById(R.id.edit).setOnClickListener(v -> {
currentEditorFile = file;
codeEditorLauncher.launch(new CodeEditorActivity.Arguments(file.path, file.name, CodeEditorActivity.EditorType.LUA_SCRIPT_EDITOR));
});
}
private void loadScript(LuaFile file) {
dismiss();
Toast.makeText(getContext(), String.format(getString(R.string.running_ff), file.name), Toast.LENGTH_SHORT).show();
new Task(() -> {
String script = FileUtils.readTextFile(file.absolutePath());
file.update();
AlberDriver.LoadLuaScript(script);
}).start();
}
@Override
public void onDestroy() {
super.onDestroy();
openDocumentLauncher.unregister();
codeEditorLauncher.unregister();
}
private static class LuaFile {
private final String name;
private final String path;
private LuaFile(String path, String name) {
this.name = name;
this.path = path;
}
private LuaFile(String name) {
this(FileUtils.getResourcesPath() + "/Lua Scripts/", name);
}
private String absolutePath() {
return path + "/" + name;
}
private void update() {
FileUtils.updateFile(absolutePath());
}
private long lastModified() {
return FileUtils.getLastModified(absolutePath());
}
}
}

View file

@ -0,0 +1,79 @@
package com.panda3ds.pandroid.app.main;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.data.game.GameMetadata;
import com.panda3ds.pandroid.utils.FileUtils;
import com.panda3ds.pandroid.utils.GameUtils;
import com.panda3ds.pandroid.view.gamesgrid.GamesGridView;
public class GamesFragment extends Fragment implements ActivityResultCallback<Uri> {
private final ActivityResultContracts.OpenDocument openRomContract = new ActivityResultContracts.OpenDocument();
private ActivityResultLauncher<String[]> pickFileRequest;
private GamesGridView gameListView;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_games, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
gameListView = view.findViewById(R.id.games);
view.findViewById(R.id.add_rom).setOnClickListener((v) -> pickFileRequest.launch(new String[] {"*/*"}));
}
@Override
public void onResume() {
super.onResume();
gameListView.setGameList(GameUtils.getGames());
}
@Override
public void onActivityResult(Uri result) {
if (result != null) {
String uri = result.toString();
if (GameUtils.findByRomPath(uri) == null) {
if (FileUtils.obtainRealPath(uri) == null) {
Toast.makeText(getContext(), "Invalid file path", Toast.LENGTH_LONG).show();
return;
}
FileUtils.makeUriPermanent(uri, FileUtils.MODE_READ);
GameMetadata game = new GameMetadata(uri, FileUtils.getName(uri).split("\\.")[0], "Unknown");
GameUtils.addGame(game);
GameUtils.launch(requireActivity(), game);
}
}
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
pickFileRequest = registerForActivityResult(openRomContract, this);
}
@Override
public void onDestroy() {
if (pickFileRequest != null) {
pickFileRequest.unregister();
pickFileRequest = null;
}
super.onDestroy();
}
}

View file

@ -0,0 +1,62 @@
package com.panda3ds.pandroid.app.main;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatEditText;
import androidx.fragment.app.Fragment;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.data.game.GameMetadata;
import com.panda3ds.pandroid.utils.GameUtils;
import com.panda3ds.pandroid.utils.SearchAgent;
import com.panda3ds.pandroid.view.SimpleTextWatcher;
import com.panda3ds.pandroid.view.gamesgrid.GamesGridView;
import java.util.ArrayList;
import java.util.List;
public class SearchFragment extends Fragment {
private final SearchAgent searchAgent = new SearchAgent();
private GamesGridView gamesListView;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_search, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
gamesListView = view.findViewById(R.id.games);
((AppCompatEditText) view.findViewById(R.id.search_bar)).addTextChangedListener((SimpleTextWatcher) this::search);
}
@Override
public void onResume() {
super.onResume();
searchAgent.clearBuffer();
for (GameMetadata game : GameUtils.getGames()) {
searchAgent.addToBuffer(game.getId(), game.getTitle(), game.getPublisher());
}
search("");
}
private void search(String query) {
List<String> resultIds = searchAgent.search(query);
ArrayList<GameMetadata> games = new ArrayList<>(GameUtils.getGames());
Object[] resultObj = games.stream().filter(gameMetadata -> resultIds.contains(gameMetadata.getId())).toArray();
games.clear();
for (Object res : resultObj) {
games.add((GameMetadata) res);
}
gamesListView.setGameList(games);
}
}

View file

@ -0,0 +1,22 @@
package com.panda3ds.pandroid.app.main;
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.app.PreferenceActivity;
import com.panda3ds.pandroid.app.base.BasePreferenceFragment;
import com.panda3ds.pandroid.app.preferences.AppearancePreferences;
import com.panda3ds.pandroid.app.preferences.DeveloperPreferences;
import com.panda3ds.pandroid.app.preferences.InputPreferences;
public class SettingsFragment extends BasePreferenceFragment {
@Override
public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
setPreferencesFromResource(R.xml.start_preferences, rootKey);
setItemClick("input", (item) -> PreferenceActivity.launch(requireContext(), InputPreferences.class));
setItemClick("appearance", (item)-> PreferenceActivity.launch(requireContext(), AppearancePreferences.class));
setItemClick("developer", (item)-> PreferenceActivity.launch(requireContext(), DeveloperPreferences.class));
}
}

View file

@ -0,0 +1,27 @@
package com.panda3ds.pandroid.app.preferences;
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.app.BaseActivity;
import com.panda3ds.pandroid.app.base.BasePreferenceFragment;
import com.panda3ds.pandroid.data.config.GlobalConfig;
import com.panda3ds.pandroid.view.preferences.SingleSelectionPreferences;
public class AppearancePreferences extends BasePreferenceFragment {
@Override
public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
setPreferencesFromResource(R.xml.appearance_preference, rootKey);
((BaseActivity) requireActivity()).getSupportActionBar().setTitle(R.string.appearance);
SingleSelectionPreferences themePreference = findPreference("theme");
themePreference.setSelectedItem(GlobalConfig.get(GlobalConfig.KEY_APP_THEME));
themePreference.setOnPreferenceChangeListener((preference, value) -> {
GlobalConfig.set(GlobalConfig.KEY_APP_THEME, (int) value);
return false;
});
}
}

View file

@ -0,0 +1,101 @@
package com.panda3ds.pandroid.app.preferences;
import android.content.pm.ActivityInfo;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.app.BaseActivity;
import com.panda3ds.pandroid.app.base.BottomAlertDialog;
import com.panda3ds.pandroid.view.controller.mapping.ControllerMapper;
import com.panda3ds.pandroid.view.controller.mapping.ControllerProfileManager;
import com.panda3ds.pandroid.view.controller.mapping.ControllerItem;
import com.panda3ds.pandroid.view.controller.mapping.Profile;
public class ControllerMapperPreferences extends Fragment {
private Profile currentProfile;
private ControllerMapper mapper;
private View saveButton;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.preference_controller_mapper, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
currentProfile = ControllerProfileManager.get(getArguments().getString("profile")).clone();
((BaseActivity) requireActivity()).getSupportActionBar().hide();
mapper = view.findViewById(R.id.mapper);
mapper.initialize(this::onLocationChanged, currentProfile);
view.findViewById(R.id.change_visibility).setOnClickListener(v -> {
BottomAlertDialog builder = new BottomAlertDialog(v.getContext());
builder.setTitle("Visibility");
boolean[] visibleList = {
currentProfile.isVisible(ControllerItem.START),
currentProfile.isVisible(ControllerItem.SELECT),
currentProfile.isVisible(ControllerItem.L),
currentProfile.isVisible(ControllerItem.R),
currentProfile.isVisible(ControllerItem.DPAD),
currentProfile.isVisible(ControllerItem.JOYSTICK),
currentProfile.isVisible(ControllerItem.GAMEPAD),
};
builder.setMultiChoiceItems(new CharSequence[]{
"Start", "Select", "L", "R", "Dpad", getString(R.string.axis), "A/B/X/Y"
}, visibleList, (dialog, index, visibility) -> {
visibleList[index] = visibility;
}).setPositiveButton(android.R.string.ok, (dialog, which) -> {
saveButton.setVisibility(View.VISIBLE);
currentProfile.setVisible(ControllerItem.START, visibleList[0]);
currentProfile.setVisible(ControllerItem.SELECT, visibleList[1]);
currentProfile.setVisible(ControllerItem.L, visibleList[2]);
currentProfile.setVisible(ControllerItem.R, visibleList[3]);
currentProfile.setVisible(ControllerItem.DPAD, visibleList[4]);
currentProfile.setVisible(ControllerItem.JOYSTICK, visibleList[5]);
currentProfile.setVisible(ControllerItem.GAMEPAD, visibleList[6]);
mapper.refreshLayout();
}).setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss());
builder.show();
});
saveButton = view.findViewById(R.id.save);
saveButton.setOnClickListener(v -> {
ControllerProfileManager.add(currentProfile);
Toast.makeText(v.getContext(), R.string.saved, Toast.LENGTH_SHORT).show();
requireActivity().finish();
});
view.findViewById(R.id.delete).setOnClickListener(v -> {
ControllerProfileManager.remove(currentProfile.getId());
requireActivity().finish();
});
view.findViewById(R.id.rotate).setOnClickListener(v -> {
requireActivity().setRequestedOrientation(mapper.getCurrentWidth() > mapper.getCurrentHeight() ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT : ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
});
view.findViewById(R.id.delete).setVisibility(ControllerProfileManager.getProfileCount() > 1 ? View.VISIBLE : View.GONE);
saveButton.setVisibility(View.GONE);
}
public void onLocationChanged(ControllerItem id) {
saveButton.setVisibility(View.VISIBLE);
}
}

View file

@ -0,0 +1,49 @@
package com.panda3ds.pandroid.app.preferences;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.preference.SwitchPreference;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.app.PandroidApplication;
import com.panda3ds.pandroid.app.base.BasePreferenceFragment;
import com.panda3ds.pandroid.app.services.LoggerService;
import com.panda3ds.pandroid.data.config.GlobalConfig;
public class DeveloperPreferences extends BasePreferenceFragment {
@Override
public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
setPreferencesFromResource(R.xml.developer_preferences, rootKey);
setActivityTitle(R.string.developer_options);
setItemClick("performanceMonitor", pref -> GlobalConfig.set(GlobalConfig.KEY_SHOW_PERFORMANCE_OVERLAY, ((SwitchPreference) pref).isChecked()));
setItemClick("shaderJit", pref -> GlobalConfig.set(GlobalConfig.KEY_SHADER_JIT, ((SwitchPreference) pref).isChecked()));
setItemClick("loggerService", pref -> {
boolean checked = ((SwitchPreference) pref).isChecked();
Context ctx = PandroidApplication.getAppContext();
if (checked) {
ctx.startService(new Intent(ctx, LoggerService.class));
} else {
ctx.stopService(new Intent(ctx, LoggerService.class));
}
GlobalConfig.set(GlobalConfig.KEY_LOGGER_SERVICE, checked);
});
refresh();
}
@Override
public void onResume() {
super.onResume();
refresh();
}
private void refresh() {
((SwitchPreference) findPreference("performanceMonitor")).setChecked(GlobalConfig.get(GlobalConfig.KEY_SHOW_PERFORMANCE_OVERLAY));
((SwitchPreference) findPreference("loggerService")).setChecked(GlobalConfig.get(GlobalConfig.KEY_LOGGER_SERVICE));
((SwitchPreference) findPreference("shaderJit")).setChecked(GlobalConfig.get(GlobalConfig.KEY_SHADER_JIT));
}
}

View file

@ -0,0 +1,76 @@
package com.panda3ds.pandroid.app.preferences;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.widget.Toast;
import androidx.activity.result.contract.ActivityResultContract;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.app.BaseActivity;
import com.panda3ds.pandroid.input.InputEvent;
import com.panda3ds.pandroid.input.InputHandler;
import java.util.Objects;
public class InputMapActivity extends BaseActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_input_map);
}
@Override
protected void onResume() {
super.onResume();
InputHandler.reset();
InputHandler.setMotionDeadZone(0.8f);
InputHandler.setEventListener(this::onInputEvent);
}
@Override
protected void onPause() {
super.onPause();
InputHandler.reset();
}
@Override
public boolean dispatchGenericMotionEvent(MotionEvent ev) {
return InputHandler.processMotionEvent(ev);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
return InputHandler.processKeyEvent(event);
}
private void onInputEvent(InputEvent event) {
if (Objects.equals(event.getName(), "KEYCODE_BACK")) {
onBackPressed();
return;
}
setResult(RESULT_OK, new Intent(event.getName()));
Toast.makeText(this, event.getName(), Toast.LENGTH_SHORT).show();
finish();
}
public static final class Contract extends ActivityResultContract<String, String> {
@NonNull
@Override
public Intent createIntent(@NonNull Context context, String s) {
return new Intent(context, InputMapActivity.class);
}
@Override
public String parseResult(int i, @Nullable Intent intent) {
return i == RESULT_OK ? intent.getAction() : null;
}
}
}

View file

@ -0,0 +1,104 @@
package com.panda3ds.pandroid.app.preferences;
import android.content.Context;
import android.os.Bundle;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import androidx.preference.SeekBarPreference;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.app.BaseActivity;
import com.panda3ds.pandroid.app.base.BasePreferenceFragment;
import com.panda3ds.pandroid.input.InputMap;
import com.panda3ds.pandroid.input.KeyName;
public class InputMapPreferences extends BasePreferenceFragment implements ActivityResultCallback<String> {
private ActivityResultLauncher<String> requestKey;
private String currentKey;
private SeekBarPreference deadZonePreference;
@Override
public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
setPreferencesFromResource(R.xml.input_map_preferences, rootKey);
((BaseActivity) requireActivity()).getSupportActionBar().setTitle(R.string.controller_mapping);
for (KeyName key : KeyName.values()) {
if (key == KeyName.NULL) {
continue;
}
setItemClick(key.name(), this::onItemPressed);
}
deadZonePreference = getPreferenceScreen().findPreference("dead_zone");
deadZonePreference.setOnPreferenceChangeListener((preference, value) -> {
InputMap.setDeadZone(((int)value / 100.0f));
refreshList();
return false;
});
refreshList();
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
requestKey = registerForActivityResult(new InputMapActivity.Contract(), this);
}
@Override
public void onDetach() {
super.onDetach();
if (requestKey != null) {
requestKey.unregister();
requestKey = null;
}
}
private void onItemPressed(Preference pref) {
currentKey = pref.getKey();
requestKey.launch(null);
}
@Override
public void onResume() {
super.onResume();
refreshList();
}
private void refreshList() {
deadZonePreference.setValue((int)(InputMap.getDeadZone() * 100));
deadZonePreference.setSummary(deadZonePreference.getValue() + "%");
for (KeyName key : KeyName.values()) {
if (key == KeyName.NULL) {
continue;
}
findPreference(key.name()).setSummary(InputMap.relative(key));
}
}
@Override
public void onActivityResult(String result) {
if (result != null) {
InputMap.set(KeyName.valueOf(currentKey), result);
refreshList();
}
}
}

View file

@ -0,0 +1,109 @@
package com.panda3ds.pandroid.app.preferences;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.app.BaseActivity;
import com.panda3ds.pandroid.app.PreferenceActivity;
import com.panda3ds.pandroid.app.base.BasePreferenceFragment;
import com.panda3ds.pandroid.app.base.BottomAlertDialog;
import com.panda3ds.pandroid.view.controller.mapping.ControllerProfileManager;
import com.panda3ds.pandroid.view.controller.mapping.Profile;
import java.util.List;
import java.util.Objects;
public class InputPreferences extends BasePreferenceFragment {
public static final String ID_DEFAULT_CONTROLLER_PROFILE = "defaultControllerProfile";
public static final String ID_INPUT_MAP = "inputMap";
public static final String ID_CREATE_PROFILE = "createProfile";
private static final CharSequence ID_GAMEPAD_PROFILE_LIST = "gamepadProfileList";
@Override
public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
setPreferencesFromResource(R.xml.input_preference, rootKey);
setItemClick(ID_INPUT_MAP, (item) -> PreferenceActivity.launch(requireContext(), InputMapPreferences.class));
setItemClick(ID_CREATE_PROFILE, (item) -> {
new BottomAlertDialog(requireContext())
.setTextInput(getString(R.string.name), (name) -> {
name = formatName(name);
if (name.length() > 0) {
Profile profile = ControllerProfileManager.makeDefaultProfile();
profile.setName(name);
ControllerProfileManager.add(profile);
refreshScreenProfileList();
} else {
Toast.makeText(requireContext(), R.string.invalid_name, Toast.LENGTH_SHORT).show();
}
}).setTitle(R.string.create_profile).show();
});
setItemClick(ID_DEFAULT_CONTROLLER_PROFILE, (item) -> {
List<Profile> profiles = ControllerProfileManager.listAll();
String defaultProfileId = ControllerProfileManager.getDefaultProfile().getId();
int defaultProfileIndex = 0;
CharSequence[] names = new CharSequence[profiles.size()];
for (int i = 0; i < names.length; i++) {
names[i] = profiles.get(i).getName();
if (Objects.equals(profiles.get(i).getId(), defaultProfileId)) {
defaultProfileIndex = i;
}
}
new BottomAlertDialog(item.getContext())
.setSingleChoiceItems(names, defaultProfileIndex, (dialog, which) -> {
dialog.dismiss();
ControllerProfileManager.setDefaultProfileId(profiles.get(which).getId());
item.setSummary(profiles.get(which).getName());
}).setTitle(R.string.pref_default_controller_title).show();
});
((BaseActivity) requireActivity()).getSupportActionBar().setTitle(R.string.input);
}
public String formatName(String name) {
return name.trim().replaceAll("\\s\\s", " ");
}
private void refresh() {
findPreference(ID_DEFAULT_CONTROLLER_PROFILE).setSummary(ControllerProfileManager.getDefaultProfile().getName());
refreshScreenProfileList();
}
@SuppressLint("RestrictedApi")
private void refreshScreenProfileList() {
PreferenceCategory category = findPreference(ID_GAMEPAD_PROFILE_LIST);
Preference add = category.getPreference(category.getPreferenceCount() - 1);
category.removeAll();
category.setOrderingAsAdded(true);
for (Profile profile : ControllerProfileManager.listAll()) {
Preference item = new Preference(category.getContext());
item.setOnPreferenceClickListener(preference -> {
category.performClick();
PreferenceActivity.launch(requireActivity(), ControllerMapperPreferences.class, new Intent().putExtra("profile", profile.getId()));
return false;
});
item.setOrder(category.getPreferenceCount());
item.setIconSpaceReserved(false);
item.setTitle(profile.getName());
category.addPreference(item);
}
add.setOrder(category.getPreferenceCount());
category.addPreference(add);
}
@Override
public void onResume() {
super.onResume();
refresh();
}
}

View file

@ -0,0 +1,104 @@
package com.panda3ds.pandroid.app.services;
import android.app.Service;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;
import androidx.annotation.Nullable;
import com.panda3ds.pandroid.lang.PipeStreamTask;
import com.panda3ds.pandroid.lang.Task;
import com.panda3ds.pandroid.utils.Constants;
import com.panda3ds.pandroid.utils.FileUtils;
import java.io.OutputStream;
import java.util.Arrays;
public class LoggerService extends Service {
private static final long MAX_LOG_SIZE = 1024 * 1024 * 4; // 4MB
private PipeStreamTask errorTask;
private PipeStreamTask outputTask;
private Process logcat;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
try {
Runtime.getRuntime().exec(new String[]{"logcat", "-c"}).waitFor();
logcat = Runtime.getRuntime().exec(new String[]{"logcat"});
String logPath = getExternalMediaDirs()[0].getAbsolutePath();
FileUtils.createDir(logPath, "logs");
logPath = logPath + "/logs";
if (FileUtils.exists(logPath + "/last.txt")) {
FileUtils.delete(logPath + "/last.txt");
}
if (FileUtils.exists(logPath + "/current.txt")) {
FileUtils.rename(logPath + "/current.txt", "last.txt");
}
OutputStream stream = FileUtils.getOutputStream(logPath + "/current.txt");
errorTask = new PipeStreamTask(logcat.getErrorStream(), stream, MAX_LOG_SIZE);
outputTask = new PipeStreamTask(logcat.getInputStream(), stream, MAX_LOG_SIZE);
errorTask.start();
outputTask.start();
Log.i(Constants.LOG_TAG, "Started logger service");
logDeviceInfo();
} catch (Exception e) {
stopSelf();
Log.e(Constants.LOG_TAG, "Failed to start logger service");
}
}
private void logDeviceInfo() {
Log.i(Constants.LOG_TAG, "----------------------");
Log.i(Constants.LOG_TAG, "Android SDK: " + Build.VERSION.SDK_INT);
Log.i(Constants.LOG_TAG, "Device: " + Build.DEVICE);
Log.i(Constants.LOG_TAG, "Model: " + Build.MANUFACTURER + " " + Build.MODEL);
Log.i(Constants.LOG_TAG, "ABIs: " + Arrays.toString(Build.SUPPORTED_ABIS));
try {
PackageInfo info = getPackageManager().getPackageInfo(getPackageName(), 0);
Log.i(Constants.LOG_TAG, "");
Log.i(Constants.LOG_TAG, "Package: " + info.packageName);
Log.i(Constants.LOG_TAG, "Install location: " + info.installLocation);
Log.i(Constants.LOG_TAG, "App version: " + info.versionName + " (" + info.versionCode + ")");
} catch (Exception e) {
Log.e(Constants.LOG_TAG, "Error obtaining package info: " + e);
}
Log.i(Constants.LOG_TAG, "----------------------");
}
@Override
public void onTaskRemoved(Intent rootIntent) {
stopSelf();
//This is a time for app save save log file
try {
Thread.sleep(1000);
} catch (Exception e) {}
super.onTaskRemoved(rootIntent);
}
@Override
public void onDestroy() {
Log.i(Constants.LOG_TAG, "Logger service terminating");
errorTask.close();
outputTask.close();
try {
logcat.destroy();
} catch (Throwable t) {}
super.onDestroy();
}
}

View file

@ -0,0 +1,39 @@
package com.panda3ds.pandroid.data;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.panda3ds.pandroid.lang.Task;
import com.panda3ds.pandroid.utils.FileUtils;
public class GsonConfigParser {
private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
private final String name;
public GsonConfigParser(String name) {
this.name = name;
}
private String getPath() {
return FileUtils.getConfigPath()+ "/" + name + ".json";
}
public void save(Object data) {
synchronized (this) {
new Task(() -> {
String json = gson.toJson(data, data.getClass());
FileUtils.writeTextFile(FileUtils.getConfigPath(), name + ".json", json);
}).runSync();
}
}
public <T> T load(Class<T> myClass) {
String[] content = new String[] {"{}"};
new Task(()->{
if (FileUtils.exists(getPath())) {
content[0] = FileUtils.readTextFile(getPath());
}
}).runSync();
return gson.fromJson(content[0], myClass);
}
}

View file

@ -0,0 +1,182 @@
package com.panda3ds.pandroid.data;
import android.graphics.Bitmap;
import android.graphics.Color;
import com.panda3ds.pandroid.data.game.GameRegion;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
public class SMDH {
public static final int LANGUAGE_JAPANESE = 0;
public static final int LANGUAGE_ENGLISH = 1;
public static final int LANGUAGE_CHINESE = 6;
public static final int LANGUAGE_KOREAN = 7;
public static final int REGION_JAPAN_MASK = 0x1;
public static final int REGION_NORTH_AMERICAN_MASK = 0x2;
public static final int REGION_EUROPE_MASK = 0x4;
public static final int REGION_AUSTRALIA_MASK = 0x8;
public static final int REGION_CHINA_MASK = 0x10;
public static final int REGION_KOREAN_MASK = 0x20;
public static final int REGION_TAIWAN_MASK = 0x40;
private static final int ICON_SIZE = 48;
private static final int META_OFFSET = 0x8;
private static final int META_REGION_OFFSET = 0x2018;
private static final int IMAGE_OFFSET = 0x24C0;
private int metaLanguage = LANGUAGE_ENGLISH;
private final ByteBuffer smdh;
private final String[] title = new String[12];
private final String[] publisher = new String[12];
private final int[] icon;
private final GameRegion region;
public SMDH(byte[] source) {
smdh = ByteBuffer.allocate(source.length);
smdh.position(0);
smdh.put(source);
smdh.position(0);
region = parseRegion();
icon = parseIcon();
parseMeta();
}
private GameRegion parseRegion() {
GameRegion region;
smdh.position(META_REGION_OFFSET);
int regionMasks = smdh.get() & 0xFF;
final boolean japan = (regionMasks & REGION_JAPAN_MASK) != 0;
final boolean northAmerica = (regionMasks & REGION_NORTH_AMERICAN_MASK) != 0;
final boolean europe = (regionMasks & REGION_EUROPE_MASK) != 0;
final boolean australia = (regionMasks & REGION_AUSTRALIA_MASK) != 0;
final boolean china = (regionMasks & REGION_CHINA_MASK) != 0;
final boolean korea = (regionMasks & REGION_KOREAN_MASK) != 0;
final boolean taiwan = (regionMasks & REGION_TAIWAN_MASK) != 0;
// Depending on the regions allowed in the region mask, pick one of the regions to use
// We prioritize English-speaking regions both here and in the emulator core, since users are most likely to speak English at least
if (northAmerica) {
region = GameRegion.NorthAmerican;
} else if (europe) {
region = GameRegion.Europe;
} else if (australia) {
region = GameRegion.Australia;
} else if (japan) {
region = GameRegion.Japan;
metaLanguage = LANGUAGE_JAPANESE;
} else if (korea) {
region = GameRegion.Korean;
metaLanguage = LANGUAGE_KOREAN;
} else if (china) {
region = GameRegion.China;
metaLanguage = LANGUAGE_CHINESE;
} else if (taiwan) {
region = GameRegion.Taiwan;
metaLanguage = LANGUAGE_CHINESE;
} else {
region = GameRegion.None;
}
return region;
}
private void parseMeta() {
byte[] data;
for (int i = 0; i < 12; i++) {
smdh.position(META_OFFSET + (512 * i) + 0x80);
data = new byte[0x100];
smdh.get(data);
title[i] = convertString(data).replaceAll("\n", " ");
smdh.position(META_OFFSET + (512 * i) + 0x180);
data = new byte[0x80];
smdh.get(data);
publisher[i] = convertString(data);
}
}
// The icons are stored in RGB562 but android need RGB888
private int[] parseIcon() {
int[] icon = new int[ICON_SIZE * ICON_SIZE];
smdh.position(0);
for (int x = 0; x < ICON_SIZE; x++) {
for (int y = 0; y < ICON_SIZE; y++) {
int indexY = y & ~7;
int indexX = x & ~7;
int interleave = mortonInterleave(x, y);
int offset = (interleave + (indexX * 8)) * 2;
offset = offset + indexY * ICON_SIZE * 2;
smdh.position(offset + IMAGE_OFFSET);
int lowByte = smdh.get() & 0xFF;
int highByte = smdh.get() & 0xFF;
int texel = (highByte << 8) | lowByte;
// Convert texel from RGB565 to RGB888
int r = (texel >> 11) & 0x1F;
int g = (texel >> 5) & 0x3F;
int b = texel & 0x1F;
r = (r << 3) | (r >> 2);
g = (g << 2) | (g >> 4);
b = (b << 3) | (b >> 2);
icon[x + ICON_SIZE * y] = Color.rgb(r, g, b);
}
}
return icon;
}
public GameRegion getRegion() {
return region;
}
public Bitmap getBitmapIcon() {
Bitmap bitmap = Bitmap.createBitmap(ICON_SIZE, ICON_SIZE, Bitmap.Config.RGB_565);
bitmap.setPixels(icon, 0, ICON_SIZE, 0, 0, ICON_SIZE, ICON_SIZE);
return bitmap;
}
public int[] getIcon() {
return icon;
}
public String getTitle() {
return title[metaLanguage];
}
public String getPublisher() {
return publisher[metaLanguage];
}
// Strings in SMDH files are stored as UTF-16LE
private static String convertString(byte[] buffer) {
try {
return new String(buffer, 0, buffer.length, StandardCharsets.UTF_16LE)
.replaceAll("\0", "");
} catch (Exception e) {
return "";
}
}
// Reference: https://github.com/wheremyfoodat/Panda3DS/blob/master/src/core/renderer_gl/textures.cpp#L88
private static int mortonInterleave(int u, int v) {
int[] xlut = {0, 1, 4, 5, 16, 17, 20, 21};
int[] ylut = {0, 2, 8, 10, 32, 34, 40, 42};
return xlut[u % 8] + ylut[v % 8];
}
}

View file

@ -0,0 +1,78 @@
package com.panda3ds.pandroid.data.config;
import com.google.gson.internal.LinkedTreeMap;
import com.panda3ds.pandroid.data.GsonConfigParser;
import com.panda3ds.pandroid.utils.Constants;
import java.io.Serializable;
import java.util.Map;
public class GlobalConfig {
private static final GsonConfigParser parser = new GsonConfigParser(Constants.PREF_GLOBAL_CONFIG);
public static final int THEME_ANDROID = 0;
public static final int THEME_LIGHT = 1;
public static final int THEME_DARK = 2;
public static final int THEME_BLACK = 3;
public static DataModel data;
public static final Key<Boolean> KEY_SHADER_JIT = new Key<>("emu.shader_jit", false);
public static final Key<Boolean> KEY_SHOW_PERFORMANCE_OVERLAY = new Key<>("dev.performanceOverlay", false);
public static final Key<Boolean> KEY_LOGGER_SERVICE = new Key<>("dev.loggerService", false);
public static final Key<Integer> KEY_APP_THEME = new Key<>("app.theme", THEME_ANDROID);
public static final Key<Boolean> KEY_SCREEN_GAMEPAD_VISIBLE = new Key<>("app.screen_gamepad.visible", true);
public static void initialize() {
data = parser.load(DataModel.class);
}
public static <T extends Serializable> T get(Key<T> key) {
Serializable value;
if (!data.configs.containsKey(key.name)) {
return key.defaultValue;
}
if (key.defaultValue instanceof String) {
value = (String) data.configs.get(key.name);
} else if (key.defaultValue instanceof Integer) {
value = ((Number) data.get(key.name)).intValue();
} else if (key.defaultValue instanceof Boolean) {
value = (boolean) data.get(key.name);
} else if (key.defaultValue instanceof Long) {
value = ((Number) data.get(key.name)).longValue();
} else {
value = ((Number) data.get(key.name)).floatValue();
}
return (T) value;
}
public static synchronized <T extends Serializable> void set(Key<T> key, T value) {
data.configs.put(key.name, value);
writeChanges();
}
private static void writeChanges() {
parser.save(data);
}
private static class Key<T extends Serializable> {
private final String name;
private final T defaultValue;
private Key(String name, T defaultValue) {
this.name = name;
this.defaultValue = defaultValue;
}
}
private static class DataModel {
private final Map<String, Object> configs = new LinkedTreeMap<>();
public Object get(String key) {
return configs.get(key);
}
}
}

View file

@ -0,0 +1,83 @@
package com.panda3ds.pandroid.data.game;
import android.graphics.Bitmap;
import android.util.Log;
import androidx.annotation.Nullable;
import com.panda3ds.pandroid.data.SMDH;
import com.panda3ds.pandroid.utils.Constants;
import com.panda3ds.pandroid.utils.GameUtils;
import java.util.Objects;
import java.util.UUID;
public class GameMetadata {
private final String id;
private final String romPath;
private final String title;
private final String publisher;
private final GameRegion[] regions;
private transient Bitmap icon;
private GameMetadata(String id, String romPath, String title, String publisher, Bitmap icon, GameRegion[] regions) {
this.id = id;
this.title = title;
this.publisher = publisher;
this.romPath = romPath;
this.regions = regions;
if (icon != null) {
GameUtils.setGameIcon(id, icon);
}
}
public GameMetadata(String romPath,String title, String publisher, GameRegion[] regions) {
this(UUID.randomUUID().toString(), romPath, title, publisher, null, regions);
}
public GameMetadata(String romPath,String title, String publisher) {
this(romPath,title, publisher, new GameRegion[]{GameRegion.None});
}
public String getRomPath() {
return romPath;
}
public String getId() {
return id;
}
public String getTitle() {
return title;
}
public String getPublisher() {
return publisher;
}
public Bitmap getIcon() {
if (icon == null) {
icon = GameUtils.loadGameIcon(id);
}
return icon;
}
public GameRegion[] getRegions() {
return regions;
}
@Override
public boolean equals(@Nullable Object obj) {
if (obj instanceof GameMetadata) {
return Objects.equals(((GameMetadata) obj).id, id);
}
return false;
}
public static GameMetadata applySMDH(GameMetadata meta, SMDH smdh) {
Bitmap icon = smdh.getBitmapIcon();
GameMetadata newMeta = new GameMetadata(meta.getId(), meta.getRomPath(), smdh.getTitle(), smdh.getPublisher(), icon, new GameRegion[]{smdh.getRegion()});
icon.recycle();
return newMeta;
}
}

View file

@ -0,0 +1,12 @@
package com.panda3ds.pandroid.data.game;
public enum GameRegion {
NorthAmerican,
Japan,
Europe,
Australia,
China,
Korean,
Taiwan,
None
}

View file

@ -0,0 +1,23 @@
package com.panda3ds.pandroid.input;
public class InputEvent {
private final String name;
private final float value;
public InputEvent(String name, float value) {
this.name = name;
this.value = Math.max(0.0f, Math.min(1.0f, value));
}
public boolean isDown() {
return value > 0.0f;
}
public String getName() {
return name;
}
public float getValue() {
return value;
}
}

View file

@ -0,0 +1,121 @@
package com.panda3ds.pandroid.input;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import com.panda3ds.pandroid.lang.Function;
import java.util.HashMap;
public class InputHandler {
private static Function<InputEvent> eventListener;
private static float motionDeadZone = 0.0f;
private static final int[] gamepadSources = {
InputDevice.SOURCE_GAMEPAD,
InputDevice.SOURCE_JOYSTICK
};
private static final int[] validSources = {
InputDevice.SOURCE_GAMEPAD,
InputDevice.SOURCE_JOYSTICK,
InputDevice.SOURCE_DPAD,
InputDevice.SOURCE_KEYBOARD
};
private static final HashMap<String, Float> motionDownEvents = new HashMap<>();
private static boolean containsSource(int[] sources, int sourceMask) {
for (int source : sources) {
if ((source & sourceMask) == source) {
return true;
}
}
return false;
}
private static boolean isGamepadSource(int sourceMask) {
return containsSource(gamepadSources, sourceMask);
}
private static boolean isSourceValid(int sourceMasked) {
return containsSource(validSources, sourceMasked);
}
public static void setEventListener(Function<InputEvent> eventListener) {
InputHandler.eventListener = eventListener;
}
private static void handleEvent(InputEvent event) {
if (eventListener != null) {
eventListener.run(event);
}
}
public static void setMotionDeadZone(float motionDeadZone) {
InputHandler.motionDeadZone = motionDeadZone;
}
public static boolean processMotionEvent(MotionEvent event) {
if (!isSourceValid(event.getSource())) {
return false;
}
if (isGamepadSource(event.getSource())) {
for (InputDevice.MotionRange range : event.getDevice().getMotionRanges()) {
float axisValue = event.getAxisValue(range.getAxis());
float value = Math.abs(axisValue);
String name = (MotionEvent.axisToString(range.getAxis()) + (axisValue >= 0 ? "+" : "-")).toUpperCase();
String reverseName = (MotionEvent.axisToString(range.getAxis()) + (axisValue >= 0 ? "-" : "+")).toUpperCase();
if (motionDownEvents.containsKey(reverseName)) {
motionDownEvents.remove(reverseName);
handleEvent(new InputEvent(reverseName.toUpperCase(), 0.0f));
}
if (value > motionDeadZone) {
motionDownEvents.put(name, value);
handleEvent(new InputEvent(name.toUpperCase(), (value - motionDeadZone) / (1.0f - motionDeadZone)));
} else if (motionDownEvents.containsKey(name)) {
motionDownEvents.remove(name);
handleEvent(new InputEvent(name.toUpperCase(), 0.0f));
}
}
}
return true;
}
public static boolean processKeyEvent(KeyEvent event) {
if (!isSourceValid(event.getSource())) {
return false;
}
if (isGamepadSource(event.getSource())) {
// Dpad return motion event + key event, this remove the key event
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_UP:
case KeyEvent.KEYCODE_DPAD_UP_LEFT:
case KeyEvent.KEYCODE_DPAD_UP_RIGHT:
case KeyEvent.KEYCODE_DPAD_DOWN:
case KeyEvent.KEYCODE_DPAD_DOWN_LEFT:
case KeyEvent.KEYCODE_DPAD_DOWN_RIGHT:
case KeyEvent.KEYCODE_DPAD_RIGHT:
case KeyEvent.KEYCODE_DPAD_LEFT:
return true;
}
}
handleEvent(new InputEvent(KeyEvent.keyCodeToString(event.getKeyCode()), event.getAction() == KeyEvent.ACTION_UP ? 0.0f : 1.0f));
return true;
}
public static void reset() {
eventListener = null;
motionDeadZone = 0.0f;
motionDownEvents.clear();
}
}

View file

@ -0,0 +1,48 @@
package com.panda3ds.pandroid.input;
import com.panda3ds.pandroid.data.GsonConfigParser;
import com.panda3ds.pandroid.utils.Constants;
public class InputMap {
public static final GsonConfigParser parser = new GsonConfigParser(Constants.PREF_INPUT_MAP);
private static DataModel data;
public static void initialize() {
data = parser.load(DataModel.class);
}
public static float getDeadZone() {
return data.deadZone;
}
public static void set(KeyName key, String name) {
data.keys[key.ordinal()] = name;
writeConfig();
}
public static String relative(KeyName key) {
return data.keys[key.ordinal()] == null ? "-" : data.keys[key.ordinal()];
}
public static KeyName relative(String name) {
for (KeyName key : KeyName.values()) {
if (relative(key).equalsIgnoreCase(name))
return key;
}
return KeyName.NULL;
}
public static void setDeadZone(float value) {
data.deadZone = Math.max(0.0f, Math.min(1.0f, value));
writeConfig();
}
private static void writeConfig() {
parser.save(data);
}
private static class DataModel {
public float deadZone = 0.2f;
public final String[] keys = new String[32];
}
}

View file

@ -0,0 +1,38 @@
package com.panda3ds.pandroid.input;
import com.panda3ds.pandroid.utils.Constants;
public enum KeyName {
A(Constants.INPUT_KEY_A),
B(Constants.INPUT_KEY_B),
X(Constants.INPUT_KEY_X),
Y(Constants.INPUT_KEY_Y),
UP(Constants.INPUT_KEY_UP),
DOWN(Constants.INPUT_KEY_DOWN),
LEFT(Constants.INPUT_KEY_LEFT),
RIGHT(Constants.INPUT_KEY_RIGHT),
AXIS_LEFT,
AXIS_RIGHT,
AXIS_UP,
AXIS_DOWN,
START(Constants.INPUT_KEY_START),
SELECT(Constants.INPUT_KEY_SELECT),
L(Constants.INPUT_KEY_L),
R(Constants.INPUT_KEY_R),
NULL;
private final int keyId;
KeyName() {
this(-1);
}
KeyName(int keyId) {
this.keyId = keyId;
}
public int getKeyId() {
return keyId;
}
}

View file

@ -0,0 +1,5 @@
package com.panda3ds.pandroid.lang;
public interface Function<T> {
void run(T arg);
}

View file

@ -0,0 +1,40 @@
package com.panda3ds.pandroid.lang;
import java.io.InputStream;
import java.io.OutputStream;
public class PipeStreamTask extends Task {
private final InputStream input;
private final OutputStream output;
private final long limit;
private long size;
public PipeStreamTask(InputStream input, OutputStream output, long limit) {
this.input = input;
this.output = output;
this.limit = limit;
}
@Override
public void run() {
super.run();
int data;
try {
while ((data = input.read()) != -1) {
output.write(data);
if (++size > limit) {
break;
}
}
} catch (Exception e) {}
close();
}
public void close() {
try {
output.flush();
output.close();
input.close();
} catch (Exception e) {}
}
}

View file

@ -0,0 +1,22 @@
package com.panda3ds.pandroid.lang;
public class Task extends Thread {
public Task(Runnable runnable) {
super(runnable);
}
protected Task() {}
public void runSync() {
start();
waitFinish();
}
public void waitFinish() {
try {
join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -0,0 +1,16 @@
package com.panda3ds.pandroid.math;
public class Vector2 {
public float x, y;
public Vector2(float x, float y) {
this.x = x;
this.y = y;
}
public static float distance(float x, float y, float x2, float y2) { return (float) Math.sqrt((x - x2) * (x - x2) + (y - y2) * (y - y2)); }
public void set(float x, float y) {
this.x = x;
this.y = y;
}
}

View file

@ -0,0 +1,29 @@
package com.panda3ds.pandroid.utils;
public class Constants {
public static final int INPUT_KEY_A = 1 << 0;
public static final int INPUT_KEY_B = 1 << 1;
public static final int INPUT_KEY_SELECT = 1 << 2;
public static final int INPUT_KEY_START = 1 << 3;
public static final int INPUT_KEY_RIGHT = 1 << 4;
public static final int INPUT_KEY_LEFT = 1 << 5;
public static final int INPUT_KEY_UP = 1 << 6;
public static final int INPUT_KEY_DOWN = 1 << 7;
public static final int INPUT_KEY_R = 1 << 8;
public static final int INPUT_KEY_L = 1 << 9;
public static final int INPUT_KEY_X = 1 << 10;
public static final int INPUT_KEY_Y = 1 << 11;
public static final int N3DS_WIDTH = 400;
public static final int N3DS_FULL_HEIGHT = 480;
public static final int N3DS_HALF_HEIGHT = N3DS_FULL_HEIGHT / 2;
public static final String ACTIVITY_PARAMETER_PATH = "path";
public static final String ACTIVITY_PARAMETER_FRAGMENT = "fragment";
public static final String LOG_TAG = "pandroid";
public static final String PREF_GLOBAL_CONFIG = "app.GlobalConfig";
public static final String PREF_GAME_UTILS = "app.GameUtils";
public static final String PREF_INPUT_MAP = "app.InputMap";
public static final String PREF_SCREEN_CONTROLLER_PROFILES = "app.input.ScreenControllerManager";
}

View file

@ -0,0 +1,246 @@
package com.panda3ds.pandroid.utils;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.system.Os;
import android.util.Log;
import androidx.documentfile.provider.DocumentFile;
import com.panda3ds.pandroid.app.PandroidApplication;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
public class FileUtils {
public static final String MODE_READ = "r";
public static final int CANONICAL_SEARCH_DEEP = 8;
private static DocumentFile parseFile(String path) {
if (path.startsWith("/")) {
return DocumentFile.fromFile(new File(path));
}
Uri uri = Uri.parse(path);
return DocumentFile.fromSingleUri(getContext(), uri);
}
private static Context getContext() {
return PandroidApplication.getAppContext();
}
public static String getName(String path) {
return parseFile(path).getName();
}
public static String getResourcesPath(){
File file = new File(getPrivatePath(), "config/resources");
if (!file.exists()) {
file.mkdirs();
}
return file.getAbsolutePath();
}
public static String getPrivatePath() {
File file = getContext().getFilesDir();
if (!file.exists()) {
file.mkdirs();
}
return file.getAbsolutePath();
}
public static String getConfigPath() {
File file = new File(getPrivatePath(), "config");
if (!file.exists()) {
file.mkdirs();
}
return file.getAbsolutePath();
}
public static boolean exists(String path) {
return parseFile(path).exists();
}
public static void rename(String path, String newName){
parseFile(path).renameTo(newName);
}
public static void delete(String path) {
DocumentFile file = parseFile(path);
if (file.exists()) {
if (file.isDirectory()) {
String[] children = listFiles(path);
for (String child : children) {
delete(path + "/" + child);
}
}
file.delete();
}
}
public static boolean createDir(String path, String name) {
DocumentFile folder = parseFile(path);
if (folder.findFile(name) != null) {
return true;
}
return folder.createDirectory(name) != null;
}
public static boolean createFile(String path, String name) {
DocumentFile folder = parseFile(path);
if (folder.findFile(name) != null) {
folder.findFile(name).delete();
}
return folder.createFile("", name) != null;
}
public static boolean writeTextFile(String path, String name, String content) {
try {
createFile(path, name);
OutputStream stream = getOutputStream(path + "/" + name);
stream.write(content.getBytes(StandardCharsets.UTF_8));
stream.flush();
stream.close();
} catch (Exception e) {
Log.e(Constants.LOG_TAG, "Error on write text file: ", e);
return false;
}
return true;
}
public static String readTextFile(String path) {
if (!exists(path)) {
return null;
}
try {
InputStream stream = getInputStream(path);
ByteArrayOutputStream output = new ByteArrayOutputStream();
int len;
byte[] buffer = new byte[1024 * 8];
while ((len = stream.read(buffer)) != -1) {
output.write(buffer, 0, len);
}
stream.close();
output.flush();
output.close();
byte[] data = output.toByteArray();
return new String(data, 0, data.length);
} catch (Exception e) {
return null;
}
}
public static InputStream getInputStream(String path) throws FileNotFoundException {
return getContext().getContentResolver().openInputStream(parseFile(path).getUri());
}
public static OutputStream getOutputStream(String path) throws FileNotFoundException {
return getContext().getContentResolver().openOutputStream(parseFile(path).getUri());
}
public static void makeUriPermanent(String uri, String mode) {
int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION;
if (mode.toLowerCase().contains("w")) {
flags &= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
}
getContext().getContentResolver().takePersistableUriPermission(Uri.parse(uri), flags);
}
/**
* When call ContentProvider.openFileDescriptor() android opens a file descriptor
* on app process in /proc/self/fd/[file descriptor id] this is a link to real file path
* can use File.getCanonicalPath() for get a link origin, but in some android version
* need use Os.readlink(path) to get a real path.
*/
public static String obtainRealPath(String uri) {
try {
ParcelFileDescriptor parcelDescriptor = getContext().getContentResolver().openFileDescriptor(Uri.parse(uri), "r");
int fd = parcelDescriptor.getFd();
File file = new File("/proc/self/fd/" + fd).getAbsoluteFile();
for (int i = 0; i < CANONICAL_SEARCH_DEEP; i++) {
try {
String canonical = file.getCanonicalPath();
if (!Objects.equals(canonical, file.getAbsolutePath())) {
file = new File(canonical).getAbsoluteFile();
}
} catch (Exception x) {
break;
}
}
if (!file.getAbsolutePath().startsWith("/proc/self/")) {
parcelDescriptor.close();
return file.getAbsolutePath();
}
String path = Os.readlink(file.getAbsolutePath());
parcelDescriptor.close();
if (new File(path).exists()) {
return path;
}
return null;
} catch (Exception e) {
return null;
}
}
public static void updateFile(String path){
DocumentFile file = parseFile(path);
Uri uri = file.getUri();
switch (uri.getScheme()) {
case "file": {
new File(uri.getPath()).setLastModified(System.currentTimeMillis());
break;
}
case "content": {
getContext().getContentResolver().update(uri, null, null, null);
break;
}
default: {
Log.w(Constants.LOG_TAG, "Cannot update file from scheme: " + uri.getScheme());
break;
}
}
}
public static long getLastModified(String path) {
return parseFile(path).lastModified();
}
public static String[] listFiles(String path){
DocumentFile folder = parseFile(path);
DocumentFile[] files = folder.listFiles();
String[] result = new String[files.length];
for (int i = 0; i < result.length; i++){
result[i] = files[i].getName();
}
return result;
}
}

View file

@ -0,0 +1,101 @@
package com.panda3ds.pandroid.utils;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.util.Log;
import com.panda3ds.pandroid.app.GameActivity;
import com.panda3ds.pandroid.data.GsonConfigParser;
import com.panda3ds.pandroid.data.game.GameMetadata;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class GameUtils {
private static final Bitmap DEFAULT_ICON = Bitmap.createBitmap(48, 48, Bitmap.Config.ARGB_8888);
public static GsonConfigParser parser = new GsonConfigParser(Constants.PREF_GAME_UTILS);
private static DataModel data;
private static GameMetadata currentGame;
public static void initialize() {
data = parser.load(DataModel.class);
}
public static GameMetadata findByRomPath(String romPath) {
for (GameMetadata game : data.games) {
if (Objects.equals(romPath, game.getRomPath())) {
return game;
}
}
return null;
}
public static void launch(Context context, GameMetadata game) {
currentGame = game;
String path = FileUtils.obtainRealPath(game.getRomPath());
context.startActivity(new Intent(context, GameActivity.class).putExtra(Constants.ACTIVITY_PARAMETER_PATH, path));
}
public static GameMetadata getCurrentGame() {
return currentGame;
}
public static void removeGame(GameMetadata game) {
data.games.remove(game);
writeChanges();
}
public static void addGame(GameMetadata game) {
data.games.add(0, game);
writeChanges();
}
public static ArrayList<GameMetadata> getGames() {
return new ArrayList<>(data.games);
}
private static void writeChanges() {
parser.save(data);
}
public static void setGameIcon(String id, Bitmap icon) {
try {
String appPath = FileUtils.getPrivatePath();
FileUtils.createDir(appPath, "cache_icons");
FileUtils.createFile(appPath + "/cache_icons/", id + ".png");
OutputStream output = FileUtils.getOutputStream(appPath + "/cache_icons/" + id + ".png");
icon.compress(Bitmap.CompressFormat.PNG, 100, output);
output.close();
} catch (Exception e) {
Log.e(Constants.LOG_TAG, "Error on save game icon: ", e);
}
}
public static Bitmap loadGameIcon(String id) {
try {
String path = FileUtils.getPrivatePath() + "/cache_icons/" + id + ".png";
if (FileUtils.exists(path)) {
InputStream stream = FileUtils.getInputStream(path);
Bitmap image = BitmapFactory.decodeStream(stream);
stream.close();
return image;
}
} catch (Exception e) {
Log.e(Constants.LOG_TAG, "Error on load game icon: ", e);
}
return DEFAULT_ICON;
}
private static class DataModel {
public final List<GameMetadata> games = new ArrayList<>();
}
}

View file

@ -0,0 +1,64 @@
package com.panda3ds.pandroid.utils;
import android.app.ActivityManager;
import android.content.Context;
import android.os.Debug;
import android.os.Process;
import com.panda3ds.pandroid.app.PandroidApplication;
import com.panda3ds.pandroid.data.config.GlobalConfig;
public class PerformanceMonitor {
private static int fps = 1;
private static String backend = "";
private static int frames = 0;
private static long lastUpdate = 0;
private static long totalMemory = 1;
private static long availableMemory = 0;
public static void initialize(String backendName) {
fps = 1;
backend = backendName;
}
public static void runFrame() {
if (GlobalConfig.get(GlobalConfig.KEY_SHOW_PERFORMANCE_OVERLAY)) {
frames++;
if (System.currentTimeMillis() - lastUpdate > 1000) {
lastUpdate = System.currentTimeMillis();
fps = frames;
frames = 0;
try {
Context ctx = PandroidApplication.getAppContext();
ActivityManager manager = (ActivityManager) ctx.getSystemService(Context.ACTIVITY_SERVICE);
ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo();
manager.getMemoryInfo(info);
totalMemory = info.totalMem;
availableMemory = info.availMem;
} catch (Exception e) {}
}
}
}
public static long getUsedMemory() {
return Math.max(1, totalMemory - availableMemory);
}
public static long getTotalMemory() {
return totalMemory;
}
public static long getAvailableMemory() {
return availableMemory;
}
public static int getFps() {
return fps;
}
public static String getBackend() {
return backend;
}
public static void destroy() {}
}

View file

@ -0,0 +1,90 @@
package com.panda3ds.pandroid.utils;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
public class SearchAgent {
// Store all results in a hashmap
// Matches IDs -> Result string
private final HashMap<String, String> searchBuffer = new HashMap<>();
// Add search item to list
public void addToBuffer(String id, String... words) {
StringBuilder string = new StringBuilder();
for (String word : words) {
string.append(normalize(word)).append(" ");
}
searchBuffer.put(id, string.toString());
}
// Convert string to lowercase alphanumeric string, converting all characters to ASCII and turning double spaces into single ones
// For example, é will be converted to e
private String normalize(String string) {
string = Normalizer.normalize(string, Normalizer.Form.NFD).replaceAll("[^\\p{ASCII}]", "");
return string.toLowerCase()
.replaceAll("(?!([a-z0-9 ])).*", "")
.replaceAll("\\s\\s", " ");
}
// Execute search and return array with item id.
public List<String> search(String query) {
String[] words = normalize(query).split("\\s");
if (words.length == 0) {
return Collections.emptyList();
}
// Map for add all search result: id -> probability
HashMap<String, Integer> results = new HashMap<>();
for (String key : searchBuffer.keySet()) {
int probability = 0;
String value = searchBuffer.get(key);
for (String word : words) {
if (value.contains(word))
probability++;
}
if (probability > 0) {
results.put(key, probability);
}
}
// Filter by probability average, ie by how closely they match to our query
// Ex: A = 10% B = 30% C = 70% (formula is (10+30+70)/3=36)
// Afterwards remove all results with probability < 36
int average = 0;
for (String key : results.keySet()) {
average += results.get(key);
}
average = average / Math.max(1, results.size());
int i = 0;
ArrayList<String> resultKeys = new ArrayList<>(Arrays.asList(results.keySet().toArray(new String[0])));
while ((i < resultKeys.size() && resultKeys.size() > 1)) {
if (results.get(resultKeys.get(i)) < average) {
String key = resultKeys.get(i);
resultKeys.remove(i);
results.remove(key);
i = 0;
continue;
}
i++;
}
return Arrays.asList(results.keySet().toArray(new String[0]));
}
// Clear search buffer
public void clearBuffer() {
searchBuffer.clear();
}
}

View file

@ -0,0 +1,153 @@
package com.panda3ds.pandroid.view;
import static android.opengl.GLES32.*;
import android.content.res.Resources;
import android.graphics.Rect;
import android.opengl.GLSurfaceView;
import android.util.Log;
import com.panda3ds.pandroid.AlberDriver;
import com.panda3ds.pandroid.data.config.GlobalConfig;
import com.panda3ds.pandroid.utils.Constants;
import com.panda3ds.pandroid.utils.GameUtils;
import com.panda3ds.pandroid.utils.PerformanceMonitor;
import com.panda3ds.pandroid.view.renderer.ConsoleRenderer;
import com.panda3ds.pandroid.view.renderer.layout.ConsoleLayout;
import com.panda3ds.pandroid.view.renderer.layout.DefaultScreenLayout;
import com.panda3ds.pandroid.data.SMDH;
import com.panda3ds.pandroid.data.game.GameMetadata;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
public class PandaGlRenderer implements GLSurfaceView.Renderer, ConsoleRenderer {
private final String romPath;
private ConsoleLayout displayLayout;
private int screenWidth, screenHeight;
private int screenTexture;
public int screenFbo;
PandaGlRenderer(String romPath) {
super();
this.romPath = romPath;
screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
screenHeight = Resources.getSystem().getDisplayMetrics().heightPixels;
setLayout(new DefaultScreenLayout());
}
@Override
protected void finalize() throws Throwable {
if (screenTexture != 0) {
glDeleteTextures(1, new int[] {screenTexture}, 0);
}
if (screenFbo != 0) {
glDeleteFramebuffers(1, new int[] {screenFbo}, 0);
}
PerformanceMonitor.destroy();
super.finalize();
}
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
Log.i(Constants.LOG_TAG, glGetString(GL_EXTENSIONS));
Log.w(Constants.LOG_TAG, glGetString(GL_VERSION));
int[] version = new int[2];
glGetIntegerv(GL_MAJOR_VERSION, version, 0);
glGetIntegerv(GL_MINOR_VERSION, version, 1);
if (version[0] < 3 || (version[0] == 3 && version[1] < 1)) {
Log.e(Constants.LOG_TAG, "OpenGL 3.1 or higher is required");
}
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
int[] generateBuffer = new int[1];
glGenTextures(1, generateBuffer, 0);
screenTexture = generateBuffer[0];
glBindTexture(GL_TEXTURE_2D, screenTexture);
glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, screenWidth, screenHeight);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glBindTexture(GL_TEXTURE_2D, 0);
glGenFramebuffers(1, generateBuffer, 0);
screenFbo = generateBuffer[0];
glBindFramebuffer(GL_FRAMEBUFFER, screenFbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, screenTexture, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
Log.e(Constants.LOG_TAG, "Framebuffer is not complete");
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
AlberDriver.Initialize();
AlberDriver.setShaderJitEnabled(GlobalConfig.get(GlobalConfig.KEY_SHADER_JIT));
AlberDriver.LoadRom(romPath);
// Load the SMDH
byte[] smdhData = AlberDriver.GetSmdh();
if (smdhData.length == 0) {
Log.w(Constants.LOG_TAG, "Failed to load SMDH");
} else {
SMDH smdh = new SMDH(smdhData);
Log.i(Constants.LOG_TAG, "Loaded rom SDMH");
Log.i(Constants.LOG_TAG, String.format("Are you playing '%s' published by '%s'", smdh.getTitle(), smdh.getPublisher()));
GameMetadata game = GameUtils.getCurrentGame();
GameUtils.removeGame(game);
GameUtils.addGame(GameMetadata.applySMDH(game, smdh));
}
PerformanceMonitor.initialize(getBackendName());
}
public void onDrawFrame(GL10 unused) {
if (AlberDriver.HasRomLoaded()) {
AlberDriver.RunFrame(screenFbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glBindFramebuffer(GL_READ_FRAMEBUFFER, screenFbo);
Rect topScreen = displayLayout.getTopDisplayBounds();
Rect bottomScreen = displayLayout.getBottomDisplayBounds();
glBlitFramebuffer(
0, Constants.N3DS_FULL_HEIGHT, Constants.N3DS_WIDTH, Constants.N3DS_HALF_HEIGHT, topScreen.left, screenHeight - topScreen.top,
topScreen.right, screenHeight - topScreen.bottom, GL_COLOR_BUFFER_BIT, GL_LINEAR
);
// Remove the black bars on the bottom screen
glBlitFramebuffer(
40, Constants.N3DS_HALF_HEIGHT, Constants.N3DS_WIDTH - 40, 0, bottomScreen.left, screenHeight - bottomScreen.top, bottomScreen.right,
screenHeight - bottomScreen.bottom, GL_COLOR_BUFFER_BIT, GL_LINEAR
);
}
PerformanceMonitor.runFrame();
}
public void onSurfaceChanged(GL10 unused, int width, int height) {
screenWidth = width;
screenHeight = height;
displayLayout.update(screenWidth, screenHeight);
}
@Override
public void setLayout(ConsoleLayout layout) {
displayLayout = layout;
displayLayout.setTopDisplaySourceSize(Constants.N3DS_WIDTH, Constants.N3DS_HALF_HEIGHT);
displayLayout.setBottomDisplaySourceSize(Constants.N3DS_WIDTH - 40 - 40, Constants.N3DS_HALF_HEIGHT);
displayLayout.update(screenWidth, screenHeight);
}
@Override
public ConsoleLayout getLayout() {
return displayLayout;
}
@Override
public String getBackendName() {
return "OpenGL";
}
}

View file

@ -0,0 +1,47 @@
package com.panda3ds.pandroid.view;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.os.Debug;
import androidx.annotation.NonNull;
import com.panda3ds.pandroid.math.Vector2;
import com.panda3ds.pandroid.view.controller.TouchEvent;
import com.panda3ds.pandroid.view.controller.nodes.TouchScreenNodeImpl;
import com.panda3ds.pandroid.view.renderer.ConsoleRenderer;
public class PandaGlSurfaceView extends GLSurfaceView implements TouchScreenNodeImpl {
final PandaGlRenderer renderer;
private int width;
private int height;
public PandaGlSurfaceView(Context context, String romPath) {
super(context);
setEGLContextClientVersion(3);
if (Debug.isDebuggerConnected()) {
setDebugFlags(DEBUG_LOG_GL_CALLS);
}
renderer = new PandaGlRenderer(romPath);
setRenderer(renderer);
}
public ConsoleRenderer getRenderer() { return renderer; }
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = getMeasuredWidth();
height = getMeasuredHeight();
}
@NonNull
@Override
public Vector2 getSize() {
return new Vector2(width, height);
}
@Override
public void onTouch(TouchEvent event) {
onTouchScreenPress(renderer, event);
}
}

View file

@ -0,0 +1,81 @@
package com.panda3ds.pandroid.view;
import android.content.Context;
import android.util.AttributeSet;
import com.panda3ds.pandroid.AlberDriver;
import com.panda3ds.pandroid.R;
import com.panda3ds.pandroid.utils.Constants;
import com.panda3ds.pandroid.view.controller.ControllerLayout;
import com.panda3ds.pandroid.view.controller.mapping.ControllerProfileManager;
import com.panda3ds.pandroid.view.controller.mapping.ControllerItem;
import com.panda3ds.pandroid.view.controller.mapping.Profile;
import com.panda3ds.pandroid.view.controller.nodes.Button;
import com.panda3ds.pandroid.view.controller.nodes.Joystick;
public class PandaLayoutController extends ControllerLayout {
private int width = -1;
private int height = -1;
public PandaLayoutController(Context context) { super(context); }
public PandaLayoutController(Context context, AttributeSet attrs) { super(context, attrs); }
public PandaLayoutController(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); }
public PandaLayoutController(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public void initialize() {
int[] keyButtonList = {R.id.button_a, Constants.INPUT_KEY_A, R.id.button_b, Constants.INPUT_KEY_B,
R.id.button_y, Constants.INPUT_KEY_Y, R.id.button_x, Constants.INPUT_KEY_X,
R.id.button_left, Constants.INPUT_KEY_LEFT, R.id.button_right, Constants.INPUT_KEY_RIGHT,
R.id.button_up, Constants.INPUT_KEY_UP, R.id.button_down, Constants.INPUT_KEY_DOWN,
R.id.button_start, Constants.INPUT_KEY_START, R.id.button_select, Constants.INPUT_KEY_SELECT,
R.id.button_l, Constants.INPUT_KEY_L, R.id.button_r, Constants.INPUT_KEY_R};
for (int i = 0; i < keyButtonList.length; i += 2) {
final int keyCode = keyButtonList[i + 1];
((Button) findViewById(keyButtonList[i])).setStateListener((btn, pressed) -> {
if (pressed)
AlberDriver.KeyDown(keyCode);
else
AlberDriver.KeyUp(keyCode);
});
}
((Joystick) findViewById(R.id.left_analog)).setJoystickListener((joystick, axisX, axisY) -> {
AlberDriver.SetCirclepadAxis((int) (axisX * 0x9C), (int) (axisY * 0x9C) * -1);
});
refreshChildren();
measure(MeasureSpec.EXACTLY, MeasureSpec.EXACTLY);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidth = getMeasuredWidth();
int measuredHeight = getMeasuredHeight();
if (measuredWidth != width || measuredHeight != height) {
width = measuredWidth;
height = measuredHeight;
applyProfileMap();
}
}
private void applyProfileMap() {
Profile profile = ControllerProfileManager.getDefaultProfile();
profile.applyToView(ControllerItem.L,findViewById(R.id.button_l), width, height);
profile.applyToView(ControllerItem.R, findViewById(R.id.button_r), width, height);
profile.applyToView(ControllerItem.START, findViewById(R.id.button_start), width, height);
profile.applyToView(ControllerItem.SELECT, findViewById(R.id.button_select), width, height);
profile.applyToView(ControllerItem.JOYSTICK, findViewById(R.id.left_analog), width, height);
profile.applyToView(ControllerItem.GAMEPAD, findViewById(R.id.gamepad), width, height);
profile.applyToView(ControllerItem.DPAD, findViewById(R.id.dpad), width, height);
}
}

View file

@ -0,0 +1,19 @@
package com.panda3ds.pandroid.view;
import android.text.Editable;
import android.text.TextWatcher;
public interface SimpleTextWatcher extends TextWatcher {
void onChange(String value);
@Override
default void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
default void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
default void afterTextChanged(Editable s) {
onChange(s.toString());
}
}

View file

@ -0,0 +1,322 @@
package com.panda3ds.pandroid.view.code;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.text.Editable;
import android.text.Layout;
import android.util.AttributeSet;
import android.view.ViewTreeObserver;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Arrays;
public class BaseEditor extends BasicTextEditor {
private static final String HELLO_WORLD = "Hello World";
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.SUBPIXEL_TEXT_FLAG);
private final Rect rect = new Rect();
private int currentLine;
private float spaceWidth;
private int lineHeight;
private int textOffset;
private int beginLine;
private int beginIndex;
private int endLine;
private int endIndex;
private int visibleHeight;
private int contentWidth;
private Layout textLayout;
private int currentWidth = -1;
private int currentHeight = -1;
private final char[] textBuffer = new char[1];
protected final int[] colors = new int[256];
// Allocate 512KB for the buffer
protected final byte[] syntaxBuffer = new byte[512 * 1024];
private boolean requireUpdate = true;
public BaseEditor(@NonNull Context context) {
super(context);
}
public BaseEditor(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public BaseEditor(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
{
EditorColors.obtainColorScheme(colors, getContext());
}
@Override
protected void initialize() {
super.initialize();
getViewTreeObserver().addOnGlobalLayoutListener(() -> {
adjustScroll();
requireUpdate = true;
});
}
@SuppressLint("MissingSuperCall")
@Override
public void draw(Canvas canvas) {
//super.draw(canvas);
canvas.drawColor(colors[EditorColors.COLOR_BACKGROUND]);
textLayout = getLayout();
if (textLayout == null) {
postDelayed(this::invalidate, 25);
return;
}
try {
prepareDraw();
if (requireUpdate) {
onVisibleContentChanged(beginIndex, endIndex - beginIndex);
}
if (getSelectionStart() == getSelectionEnd()) {
drawCaret(canvas);
drawCurrentLine(canvas);
} else {
drawSelection(canvas);
}
drawText(canvas);
drawLineCount(canvas);
} catch (Throwable e) {
drawError(canvas, e);
}
}
private void drawError(Canvas canvas, Throwable e) {
canvas.drawColor(Color.RED);
paint.setTextSize(getTextSize());
paint.setColor(Color.WHITE);
canvas.drawText("Editor draw error:", getPaddingLeft(), getLineHeight(), paint);
canvas.drawText(String.valueOf(e), getPaddingLeft(), getLineHeight() * 2, paint);
int index = 2;
for (StackTraceElement trace : e.getStackTrace()) {
index++;
if (index > 5) break;
canvas.drawText(trace.getClassName() + ":" + trace.getMethodName() + ":" + trace.getLineNumber(), getPaddingLeft(), getLineHeight() * index, paint);
}
}
private void prepareDraw() {
paint.setTypeface(getTypeface());
paint.setTextSize(getTextSize());
Paint.FontMetrics fontMetrics = paint.getFontMetrics();
spaceWidth = paint.measureText(" ");
lineHeight = getLineHeight();
//Align text to center of line
{
int ascent = (int) Math.abs(fontMetrics.ascent);
paint.getTextBounds(HELLO_WORLD, 0, HELLO_WORLD.length(), rect);
textOffset = Math.max(((lineHeight - rect.height()) / 2), 0) + ascent;
}
int lineCount = textLayout.getLineCount();
currentLine = textLayout.getLineForOffset(getSelectionStart());
int oldBeginLine = beginLine;
int oldEndLine = endLine;
beginLine = Math.max(0, Math.min((getScrollY() / lineHeight) - 1, lineCount));
beginIndex = textLayout.getLineStart(beginLine);
if (oldEndLine != endLine || beginLine != oldBeginLine) {
requireUpdate = true;
}
getGlobalVisibleRect(rect);
visibleHeight = rect.height();
endLine = Math.round(((float) visibleHeight / lineHeight) + 2) + beginLine;
endIndex = getLayout().getLineStart(Math.min(lineCount, endLine));
int padding = (int) (paint.measureText(String.valueOf(lineCount)) + (spaceWidth * 4));
if (getPaddingLeft() != padding) {
setPadding(padding, 0, 0, 0);
}
contentWidth = getWidth() + getScrollX();
}
private void drawLineCount(Canvas canvas) {
int colorEnable = colors[EditorColors.COLOR_TEXT];
int colorDisable = applyAlphaToColor(colors[EditorColors.COLOR_TEXT], 100);
paint.setColor(colors[EditorColors.COLOR_BACKGROUND_SECONDARY]);
int scrollY = getScrollY();
float x = getScrollX();
canvas.translate(x, 0);
canvas.drawRect(0, scrollY, getPaddingLeft() - spaceWidth, visibleHeight + scrollY, paint);
paint.setColor(colors[EditorColors.COLOR_CURRENT_LINE]);
canvas.drawRect(0, currentLine * lineHeight, getPaddingLeft() - spaceWidth, (currentLine * lineHeight) + lineHeight, paint);
for (int i = beginLine; i < Math.min(getLineCount(), endLine); i++) {
String text = String.valueOf(i + 1);
if (i == currentLine) {
paint.setColor(colorEnable);
} else {
paint.setColor(colorDisable);
}
float width = paint.measureText(text);
canvas.drawText(text, getPaddingLeft() - width - (spaceWidth * 2.5f), (i * lineHeight) + textOffset, paint);
}
paint.setColor(applyAlphaToColor(colorEnable, 10));
canvas.drawRect(getPaddingLeft() - spaceWidth - (spaceWidth / 4), scrollY, getPaddingLeft() - spaceWidth, visibleHeight + scrollY, paint);
canvas.translate(-x, 0);
}
private void drawCurrentLine(Canvas canvas) {
float y = currentLine * lineHeight;
paint.setColor(colors[EditorColors.COLOR_CURRENT_LINE]);
canvas.drawRect(0, y, contentWidth, y + lineHeight, paint);
}
private void drawText(Canvas canvas) {
Editable edit = getText();
float x = 0;
float y = textOffset;
int line = 0;
canvas.translate(getPaddingLeft(), beginLine * lineHeight);
paint.setColor(colors[EditorColors.COLOR_TEXT]);
for (int i = beginIndex; i < endIndex; i++) {
textBuffer[0] = edit.charAt(i);
switch (textBuffer[0]) {
case '\n':
line++;
x = 0;
y = (line * lineHeight) + textOffset;
break;
case ' ':
x += spaceWidth;
break;
default:
paint.setColor(colors[syntaxBuffer[i - beginIndex]]);
canvas.drawText(textBuffer, 0, 1, x, y, paint);
x += paint.measureText(textBuffer, 0, 1);
break;
}
}
canvas.translate(-getPaddingLeft(), -(beginLine * lineHeight));
}
private void drawCaret(Canvas canvas) {
int start = textLayout.getLineStart(currentLine);
int end = textLayout.getLineEnd(currentLine);
int position = getSelectionStart();
float x = getPaddingLeft();
float y = (currentLine * lineHeight);
Editable text = getText();
for (int i = start; i < end; i++) {
if (i == position) {
break;
}
textBuffer[0] = text.charAt(i);
x += paint.measureText(textBuffer, 0, 1);
}
paint.setColor(colors[EditorColors.COLOR_CARET]);
float caretWidth = spaceWidth / 2;
canvas.drawRect(x - (caretWidth / 2), y, x + (caretWidth / 2), y + lineHeight, paint);
}
private void drawSelection(Canvas canvas) {
int start = getSelectionStart();
int end = getSelectionEnd();
int endLine = textLayout.getLineForOffset(end);
canvas.translate(getPaddingLeft(), 0);
paint.setColor(colors[EditorColors.COLOR_SELECTION]);
Editable text = getText();
for (int line = currentLine; line <= endLine; line++) {
if (line < beginLine) continue;
if (line > this.endLine) break;
if (line == endLine || line == currentLine) {
int lineStart = textLayout.getLineStart(line);
float x = 0;
if (lineStart <= start) {
x = paint.measureText(text, lineStart, start);
lineStart = start;
}
float width;
if (line < endLine) {
width = contentWidth;
} else {
width = paint.measureText(text, lineStart, end);
}
canvas.drawRect(x, lineHeight * line, x + width, (lineHeight * line) + lineHeight, paint);
} else {
canvas.drawRect(0, lineHeight * line, contentWidth, (lineHeight * line) + lineHeight, paint);
}
}
canvas.translate(-getPaddingLeft(), 0);
}
public int applyAlphaToColor(int color, int alpha) {
return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color));
}
protected void onVisibleContentChanged(int index, int length) {
requireUpdate = false;
Arrays.fill(syntaxBuffer, (byte) 0);
if (length > 0) {
onRefreshColorScheme(syntaxBuffer, index, length);
}
}
protected void onRefreshColorScheme(byte[] buffer, int index, int length) {
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (currentWidth != getMeasuredWidth() || currentHeight != getMeasuredHeight()) {
currentWidth = getMeasuredWidth();
currentHeight = getMeasuredHeight();
invalidateAll();
}
}
protected void invalidateAll() {
requireUpdate = true;
invalidate();
}
@Override
protected void onTextChanged() {
requireUpdate = true;
super.onTextChanged();
}
}

View file

@ -0,0 +1,154 @@
package com.panda3ds.pandroid.view.code;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.inputmethod.EditorInfo;
import android.widget.Scroller;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatEditText;
import com.panda3ds.pandroid.view.SimpleTextWatcher;
public class BasicTextEditor extends AppCompatEditText {
private GestureDetector gestureDetector;
private final Rect visibleRect = new Rect();
public BasicTextEditor(@NonNull Context context) {
super(context);
initialize();
}
public BasicTextEditor(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initialize();
}
public BasicTextEditor(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize();
}
protected void initialize() {
setTypeface(Typeface.MONOSPACE);
gestureDetector = new GestureDetector(getContext(), new ScrollGesture());
setTypeface(Typeface.createFromAsset(getContext().getAssets(), "fonts/comic_mono.ttf"));
setGravity(Gravity.START | Gravity.TOP);
setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
setLineSpacing(0, 1.3f);
setScroller(new Scroller(getContext()));
setInputType(InputType.TYPE_CLASS_TEXT |
InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS |
InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE |
InputType.TYPE_TEXT_FLAG_MULTI_LINE |
InputType.TYPE_TEXT_FLAG_AUTO_CORRECT);
setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI);
setBackgroundColor(Color.BLACK);
setTextColor(Color.WHITE);
setFocusableInTouchMode(true);
setHorizontallyScrolling(true);
setHorizontalScrollBarEnabled(true);
addTextChangedListener((SimpleTextWatcher) value -> BasicTextEditor.this.onTextChanged());
}
// Disable default Android scroll
@Override
public void scrollBy(int x, int y) {}
@Override
public void scrollTo(int x, int y) {}
public void setScroll(int x, int y) {
x = Math.max(0, x);
y = Math.max(0, y);
int maxHeight = Math.round(getLineCount() * getLineHeight());
getGlobalVisibleRect(visibleRect);
maxHeight = Math.max(0, maxHeight - visibleRect.height());
int maxWidth = (int) getPaint().measureText(getText(), 0, length());
maxWidth += getPaddingLeft() + getPaddingRight();
int scrollX = x - Math.max(Math.min(maxWidth - visibleRect.width(), x), 0);
int scrollY = Math.min(maxHeight, y);
super.scrollTo(scrollX, scrollY);
}
public void adjustScroll(){
setScroll(getScrollX(), getScrollY());
}
protected void onTextChanged() {}
private boolean onSuperTouchListener(MotionEvent event) {
return super.onTouchEvent(event);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
return gestureDetector.onTouchEvent(event);
}
private class ScrollGesture implements GestureDetector.OnGestureListener {
@Override
public boolean onDown(@NonNull MotionEvent e) {
return true;
}
@Override
public void onShowPress(@NonNull MotionEvent e) {
onSuperTouchListener(e);
}
@Override
public boolean onSingleTapUp(@NonNull MotionEvent e) {
return onSuperTouchListener(e);
}
@Override
public boolean onScroll(@NonNull MotionEvent e1, @NonNull MotionEvent e2, float distanceX, float distanceY) {
int scrollX = (int) Math.max(0, getScrollX() + distanceX);
int scrollY = (int) Math.max(0, getScrollY() + distanceY);
setScroll(scrollX, scrollY);
return true;
}
@Override
public void onLongPress(@NonNull MotionEvent e) {
onSuperTouchListener(e);
}
@Override
public boolean onFling(@NonNull MotionEvent e1, @NonNull MotionEvent e2, float velocityX, float velocityY) {
return false;
}
}
public void insert(CharSequence text) {
if (getSelectionStart() == getSelectionEnd()) {
getText().insert(getSelectionStart(), text);
} else {
getText().replace(getSelectionStart(), getSelectionEnd(), text);
}
}
}

View file

@ -0,0 +1,51 @@
package com.panda3ds.pandroid.view.code;
import android.content.Context;
import android.util.AttributeSet;
import com.panda3ds.pandroid.view.code.syntax.CodeSyntax;
public class CodeEditor extends BaseEditor {
private CodeSyntax syntax;
private Runnable contentChangeListener;
public CodeEditor(Context context) {
super(context);
}
public CodeEditor(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CodeEditor(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setSyntax(CodeSyntax syntax) {
this.syntax = syntax;
invalidateAll();
}
public void setOnContentChangedListener(Runnable contentChangeListener) {
this.contentChangeListener = contentChangeListener;
}
@Override
protected void onTextChanged() {
super.onTextChanged();
if (contentChangeListener != null) {
contentChangeListener.run();
}
}
@Override
protected void onRefreshColorScheme(byte[] buffer, int index, int length) {
super.onRefreshColorScheme(buffer, index, length);
if (syntax != null) {
final CharSequence text = getText().subSequence(index, index + length);
syntax.apply(syntaxBuffer, text);
System.gc();
}
}
}

View file

@ -0,0 +1,63 @@
package com.panda3ds.pandroid.view.code;
import android.content.Context;
import com.panda3ds.pandroid.app.PandroidApplication;
public class EditorColors {
public static final byte COLOR_TEXT = 0x0;
public static final byte COLOR_KEYWORDS = 0x1;
public static final byte COLOR_NUMBERS = 0x2;
public static final byte COLOR_STRING = 0x3;
public static final byte COLOR_METADATA = 0x4;
public static final byte COLOR_COMMENT = 0x5;
public static final byte COLOR_SYMBOLS = 0x6;
public static final byte COLOR_FIELDS = 0x7;
public static final byte COLOR_BACKGROUND = 0x1D;
public static final byte COLOR_BACKGROUND_SECONDARY = 0x2D;
public static final byte COLOR_SELECTION = 0x3D;
public static final byte COLOR_CARET = 0x4D;
public static final byte COLOR_CURRENT_LINE = 0x5D;
public static void obtainColorScheme(int[] colors, Context context) {
if (PandroidApplication.isDarkMode()) {
applyDarkTheme(colors);
} else {
applyLightTheme(colors);
}
}
private static void applyLightTheme(int[] colors) {
colors[EditorColors.COLOR_TEXT] = 0xFF000000;
colors[EditorColors.COLOR_KEYWORDS] = 0xFF3AE666;
colors[EditorColors.COLOR_NUMBERS] = 0xFF3A9EE6;
colors[EditorColors.COLOR_METADATA] = 0xFF806AE6;
colors[EditorColors.COLOR_SYMBOLS] = 0xFF202020;
colors[EditorColors.COLOR_STRING] = 0xFF2EB541;
colors[EditorColors.COLOR_FIELDS] = 0xFF9876AA;
colors[EditorColors.COLOR_COMMENT] = 0xFF808080;
colors[EditorColors.COLOR_BACKGROUND] = 0xFFFFFFFF;
colors[EditorColors.COLOR_BACKGROUND_SECONDARY] = 0xFFF0F0F0;
colors[EditorColors.COLOR_SELECTION] = 0x701F9EDE;
colors[EditorColors.COLOR_CARET] = 0xFF000000;
colors[EditorColors.COLOR_CURRENT_LINE] = 0x05000050;
}
private static void applyDarkTheme(int[] colors) {
colors[EditorColors.COLOR_TEXT] = 0xFFFFFFFF;
colors[EditorColors.COLOR_KEYWORDS] = 0xFFE37F3E;
colors[EditorColors.COLOR_NUMBERS] = 0xFF3A9EE6;
colors[EditorColors.COLOR_METADATA] = 0xFFC5CA1D;
colors[EditorColors.COLOR_SYMBOLS] = 0xFFC0C0C0;
colors[EditorColors.COLOR_STRING] = 0xFF2EB541;
colors[EditorColors.COLOR_FIELDS] = 0xFF9876AA;
colors[EditorColors.COLOR_COMMENT] = 0xFFBBBBBB;
colors[EditorColors.COLOR_BACKGROUND] = 0xFF2B2B2B;
colors[EditorColors.COLOR_BACKGROUND_SECONDARY] = 0xFF313335;
colors[EditorColors.COLOR_SELECTION] = 0x701F9EDE;
colors[EditorColors.COLOR_CARET] = 0x60FFFFFF;
colors[EditorColors.COLOR_CURRENT_LINE] = 0x10FFFFFF;
}
}

View file

@ -0,0 +1,21 @@
package com.panda3ds.pandroid.view.code.syntax;
public abstract class CodeSyntax {
public abstract void apply(byte[] syntaxBuffer, final CharSequence text);
// Get syntax highlighting data for a file based on its filename, by looking at the extension
public static CodeSyntax getFromFilename(String name) {
name = name.trim().toLowerCase();
String[] parts = name.split("\\.");
if (parts.length == 0)
return null;
// Get syntax based on file extension
switch (parts[parts.length - 1]) {
case "lua":
return new LuaSyntax();
default:
return null;
}
}
}

View file

@ -0,0 +1,58 @@
package com.panda3ds.pandroid.view.code.syntax;
import com.panda3ds.pandroid.view.code.EditorColors;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class LuaSyntax extends CodeSyntax {
public static final Pattern comment = Pattern.compile("(\\-\\-.*)");
public static final Pattern keywords = PatternUtils.buildGenericKeywords(
"and", "break", "do", "else", "elseif", "end", "false", "for", "function", "if", "in",
"local", "nil", "not", "or", "repeat", "return", "then", "true", "until", "while");
public static final Pattern identifiers = PatternUtils.buildGenericKeywords(
"assert", "collectgarbage", "dofile", "error", "getmetatable", "ipairs", "loadfile", "load", "loadstring", "next", "pairs", "pcall", "print", "rawequal", "rawlen", "rawget", "rawset",
"select", "setmetatable", "tonumber", "tostring", "type", "xpcall", "_G", "_VERSION", "arshift", "band", "bnot", "bor", "bxor", "btest", "extract", "lrotate", "lshift", "replace",
"rrotate", "rshift", "create", "resume", "running", "status", "wrap", "yield", "isyieldable", "debug", "getuservalue", "gethook", "getinfo", "getlocal", "getregistry", "getmetatable",
"getupvalue", "upvaluejoin", "upvalueid", "setuservalue", "sethook", "setlocal", "setmetatable", "setupvalue", "traceback", "close", "flush", "input", "lines", "open", "output", "popen",
"read", "tmpfile", "type", "write", "close", "flush", "lines", "read", "seek", "setvbuf", "write", "__gc", "__tostring", "abs", "acos", "asin", "atan", "ceil", "cos", "deg", "exp", "tointeger",
"floor", "fmod", "ult", "log", "max", "min", "modf", "rad", "random", "randomseed", "sin", "sqrt", "string", "tan", "type", "atan2", "cosh", "sinh", "tanh",
"pow", "frexp", "ldexp", "log10", "pi", "huge", "maxinteger", "mininteger", "loadlib", "searchpath", "seeall", "preload", "cpath", "path", "searchers", "loaded", "module", "require", "clock",
"date", "difftime", "execute", "exit", "getenv", "remove", "rename", "setlocale", "time", "tmpname", "byte", "char", "dump", "find", "format", "gmatch", "gsub", "len", "lower", "match", "rep",
"reverse", "sub", "upper", "pack", "packsize", "unpack", "concat", "maxn", "insert", "pack", "unpack", "remove", "move", "sort", "offset", "codepoint", "char", "len", "codes", "charpattern",
"coroutine", "table", "io", "os", "string", "uint8_t", "bit32", "math", "debug", "package");
public static final Pattern string = Pattern.compile("((\")(.*?)([^\\\\]\"))|((\")(.+))|((')(.?)('))");
public static final Pattern symbols = Pattern.compile("([.!&?:;*+/{}()\\]\\[,=-])");
public static final Pattern numbers = Pattern.compile("\\b((\\d*[.]?\\d+([Ee][+-]?[\\d]+)?[LlfFdD]?)|(0[xX][0-9a-zA-Z]+)|(0[bB][0-1]+)|(0[0-7]+))\\b");
@Override
public void apply(byte[] syntaxBuffer, CharSequence text) {
for (Matcher matcher = keywords.matcher(text); matcher.find(); ) {
Arrays.fill(syntaxBuffer, matcher.start(), matcher.end(), EditorColors.COLOR_KEYWORDS);
}
for (Matcher matcher = identifiers.matcher(text); matcher.find(); ) {
Arrays.fill(syntaxBuffer, matcher.start(), matcher.end(), EditorColors.COLOR_FIELDS);
}
for (Matcher matcher = symbols.matcher(text); matcher.find(); ) {
Arrays.fill(syntaxBuffer, matcher.start(), matcher.end(), EditorColors.COLOR_SYMBOLS);
}
for (Matcher matcher = numbers.matcher(text); matcher.find(); ) {
Arrays.fill(syntaxBuffer, matcher.start(), matcher.end(), EditorColors.COLOR_NUMBERS);
}
for (Matcher matcher = string.matcher(text); matcher.find(); ) {
Arrays.fill(syntaxBuffer, matcher.start(), matcher.end(), EditorColors.COLOR_STRING);
}
for (Matcher matcher = comment.matcher(text); matcher.find(); ) {
Arrays.fill(syntaxBuffer, matcher.start(), matcher.end(), EditorColors.COLOR_COMMENT);
}
}
}

View file

@ -0,0 +1,18 @@
package com.panda3ds.pandroid.view.code.syntax;
import java.util.regex.Pattern;
class PatternUtils {
public static Pattern buildGenericKeywords(String... keywords){
StringBuilder builder = new StringBuilder();
builder.append("\\b(");
for (int i = 0; i < keywords.length; i++){
builder.append(keywords[i]);
if (i+1 != keywords.length){
builder.append("|");
}
}
builder.append(")\\b");
return Pattern.compile(builder.toString());
}
}

View file

@ -0,0 +1,143 @@
package com.panda3ds.pandroid.view.controller;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
import com.panda3ds.pandroid.math.Vector2;
import com.panda3ds.pandroid.utils.Constants;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
public class ControllerLayout extends RelativeLayout {
private final HashMap<Integer, ControllerNode> activeTouchEvents = new HashMap<>();
private final ArrayList<ControllerNode> controllerNodes = new ArrayList<>();
public ControllerLayout(Context context) { this(context, null); }
public ControllerLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); }
public ControllerLayout(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); }
public ControllerLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public void refreshChildren() {
ArrayList<ControllerNode> nodes = new ArrayList<>();
populateNodesArray(this, nodes);
// Need Reverse: First view is in back and last view is in front for respect android View hierarchy
Collections.reverse(nodes);
controllerNodes.clear();
controllerNodes.addAll(nodes);
}
private void populateNodesArray(ViewGroup group, ArrayList<ControllerNode> list) {
for (int i = 0; i < group.getChildCount(); i++) {
View view = group.getChildAt(i);
if (view instanceof ControllerNode) {
list.add((ControllerNode) view);
} else if (view instanceof ViewGroup) {
populateNodesArray((ViewGroup) view, list);
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int index = event.getActionIndex();
switch (event.getActionMasked()) {
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_POINTER_UP: {
int id = event.getPointerId(index);
processTouch(true, event.getX(index), event.getY(index), id);
} break;
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN: {
int id = event.getPointerId(index);
processTouch(false, event.getX(index), event.getY(index), id);
} break;
case MotionEvent.ACTION_MOVE:
for (int id = 0; id < event.getPointerCount(); id++) {
processTouch(false, event.getX(id), event.getY(id), id);
}
break;
}
return true;
}
private void processTouch(boolean up, float x, float y, int index) {
int[] globalPosition = new int[2];
getLocationInWindow(globalPosition);
TouchType action = TouchType.ACTION_MOVE;
if ((!activeTouchEvents.containsKey(index))) {
if (up) return;
ControllerNode node = null;
for (ControllerNode item : controllerNodes) {
Vector2 pos = item.getPosition();
Vector2 size = item.getSize();
float cx = (pos.x - globalPosition[0]);
float cy = (pos.y - globalPosition[1]);
if (item.isVisible() && x > cx && x < cx + size.x && y > cy && y < cy + size.y) {
node = item;
break;
}
}
if (node != null) {
activeTouchEvents.put(index, node);
action = TouchType.ACTION_DOWN;
} else {
return;
}
}
if (up) action = TouchType.ACTION_UP;
ControllerNode node = activeTouchEvents.get(index);
Vector2 pos = node.getPosition();
pos.x -= globalPosition[0];
pos.y -= globalPosition[1];
x -= pos.x;
y -= pos.y;
node.onTouch(new TouchEvent(x, y, action));
if (up) {
activeTouchEvents.remove(index);
}
}
@Override
public void onViewAdded(View child) {
super.onViewAdded(child);
refreshChildren();
}
@Override
public void onViewRemoved(View child) {
super.onViewRemoved(child);
refreshChildren();
}
// TODO: Need to replace these methods to prevent Android sending events directly to children
@Override
public ArrayList<View> getTouchables() {
return new ArrayList<>();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return true;
}
}

View file

@ -0,0 +1,21 @@
package com.panda3ds.pandroid.view.controller;
import android.view.View;
import androidx.annotation.NonNull;
import com.panda3ds.pandroid.math.Vector2;
public interface ControllerNode {
@NonNull
default Vector2 getPosition() {
View view = (View) this;
int[] position = new int[2];
view.getLocationInWindow(position);
return new Vector2(position[0], position[1]);
}
default boolean isVisible() { return ((View) this).isShown(); }
@NonNull Vector2 getSize();
void onTouch(TouchEvent event);
}

View file

@ -0,0 +1,16 @@
package com.panda3ds.pandroid.view.controller;
public class TouchEvent {
private final TouchType action;
private final float x, y;
public float getX() { return x; }
public float getY() { return y; }
public TouchType getAction() { return action; }
public TouchEvent(float x, float y, TouchType action) {
this.x = x;
this.y = y;
this.action = action;
}
}

Some files were not shown because too many files have changed in this diff Show more