mirror of
https://github.com/wheremyfoodat/Panda3DS.git
synced 2025-07-23 07:13:00 +12:00
Merge branch 'master' into delete-emu
This commit is contained in:
commit
8cee60ebf5
286 changed files with 13182 additions and 508 deletions
|
@ -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)) {
|
||||
|
|
|
@ -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
|
947
src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp
Normal file
947
src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp
Normal 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
|
|
@ -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]);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
32
src/core/applets/error_applet.cpp
Normal file
32
src/core/applets/error_applet.cpp
Normal 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;
|
||||
}
|
|
@ -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, ¶meters[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(¶m.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;
|
||||
}
|
||||
|
|
|
@ -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, ¶meters[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(¶m.data[0], &config, sizeof(config));
|
||||
|
||||
nextParameter = param;
|
||||
}
|
|
@ -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!");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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()) {
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
|
134
src/emulator.cpp
134
src/emulator.cpp
|
@ -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) {
|
||||
|
|
|
@ -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
116
src/jni_driver.cpp
Normal 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
|
25
src/lua.cpp
25
src/lua.cpp
|
@ -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
|
||||
|
|
63
src/panda_qt/about_window.cpp
Normal file
63
src/panda_qt/about_window.cpp
Normal 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);
|
||||
}
|
268
src/panda_qt/cheats_window.cpp
Normal file
268
src/panda_qt/cheats_window.cpp
Normal 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();
|
||||
}
|
99
src/panda_qt/config_window.cpp
Normal file
99
src/panda_qt/config_window.cpp
Normal 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; }
|
|
@ -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);
|
||||
}
|
44
src/panda_qt/text_editor.cpp
Normal file
44
src/panda_qt/text_editor.cpp
Normal 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
2
src/panda_qt/zep.cpp
Normal file
|
@ -0,0 +1,2 @@
|
|||
#define ZEP_SINGLE_HEADER_BUILD
|
||||
#include "zep.h"
|
15
src/pandroid/.gitignore
vendored
Normal file
15
src/pandroid/.gitignore
vendored
Normal 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
|
56
src/pandroid/app/build.gradle.kts
Normal file
56
src/pandroid/app/build.gradle.kts
Normal 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
21
src/pandroid/app/proguard-rules.pro
vendored
Normal 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
|
54
src/pandroid/app/src/main/AndroidManifest.xml
Normal file
54
src/pandroid/app/src/main/AndroidManifest.xml
Normal 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>
|
BIN
src/pandroid/app/src/main/assets/fonts/comic_mono.ttf
Normal file
BIN
src/pandroid/app/src/main/assets/fonts/comic_mono.ttf
Normal file
Binary file not shown.
|
@ -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"); }
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package com.panda3ds.pandroid.data.game;
|
||||
|
||||
public enum GameRegion {
|
||||
NorthAmerican,
|
||||
Japan,
|
||||
Europe,
|
||||
Australia,
|
||||
China,
|
||||
Korean,
|
||||
Taiwan,
|
||||
None
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package com.panda3ds.pandroid.lang;
|
||||
|
||||
public interface Function<T> {
|
||||
void run(T arg);
|
||||
}
|
|
@ -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) {}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<>();
|
||||
}
|
||||
}
|
|
@ -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() {}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue