mirror of
https://github.com/wheremyfoodat/Panda3DS.git
synced 2025-06-08 20:11:39 +12:00
Merge branch 'master' into specialized-shaderz
This commit is contained in:
commit
9dc12e0681
77 changed files with 2403 additions and 305 deletions
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -4,13 +4,14 @@
|
|||
|
||||
using namespace Applets;
|
||||
|
||||
AppletManager::AppletManager(Memory& mem) : miiSelector(mem, nextParameter), swkbd(mem, nextParameter) {}
|
||||
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) {
|
||||
|
@ -21,6 +22,9 @@ AppletBase* AppletManager::getApplet(u32 id) {
|
|||
case AppletIDs::SoftwareKeyboard:
|
||||
case AppletIDs::SoftwareKeyboard2: return &swkbd;
|
||||
|
||||
case AppletIDs::ErrDisp:
|
||||
case AppletIDs::ErrDisp2: return &error;
|
||||
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
|
|
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));
|
||||
|
||||
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) {
|
||||
Helpers::warn("Mii Selector: Unimplemented ReceiveParameter");
|
||||
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,20 +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(const Applets::Parameter& parameter) {
|
||||
Helpers::warn("Software keyboard: Unimplemented ReceiveParameter");
|
||||
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 = parameter.destID,
|
||||
.senderID = appID,
|
||||
.destID = AppletIDs::Application,
|
||||
.signal = static_cast<u32>(APTSignal::Response),
|
||||
.data = {},
|
||||
.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;
|
||||
return Result::Success;
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
#include "cheats.hpp"
|
||||
#include "swap.hpp"
|
||||
|
||||
Cheats::Cheats(Memory& mem, HIDService& hid) : ar(mem, hid) { reset(); }
|
||||
|
||||
|
@ -23,6 +24,27 @@ u32 Cheats::addCheat(const Cheat& 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;
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
@ -13,27 +16,44 @@ Handle Kernel::makeTimer(ResetType type) {
|
|||
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) {
|
||||
|
@ -54,6 +74,8 @@ void Kernel::signalTimer(Handle timerHandle, Timer* timer) {
|
|||
|
||||
if (timer->interval == 0) {
|
||||
cancelTimer(timer);
|
||||
} else {
|
||||
timer->fireTick = cpu.getTicks() + Scheduler::nsToCycles(timer->interval);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,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;
|
||||
}
|
||||
|
||||
|
|
|
@ -64,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;
|
||||
|
@ -140,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));
|
||||
|
@ -222,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);
|
||||
|
@ -260,7 +294,9 @@ 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();
|
||||
|
@ -274,14 +310,21 @@ void APTService::receiveParameter(u32 messagePointer) {
|
|||
// 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();
|
||||
|
@ -296,8 +339,13 @@ void APTService::glanceParameter(u32 messagePointer) {
|
|||
// 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) {
|
||||
|
|
|
@ -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;
|
||||
|
@ -764,3 +767,22 @@ void FSService::renameFile(u32 messagePointer) {
|
|||
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);
|
||||
}
|
|
@ -18,6 +18,7 @@ namespace ServiceCommands {
|
|||
ReleaseRight = 0x00170000,
|
||||
ImportDisplayCaptureInfo = 0x00180000,
|
||||
SaveVramSysArea = 0x00190000,
|
||||
RestoreVramSysArea = 0x001A0000,
|
||||
SetInternalPriorities = 0x001E0080,
|
||||
StoreDataCache = 0x001F0082
|
||||
};
|
||||
|
@ -51,6 +52,7 @@ void GPUService::handleSyncRequest(u32 messagePointer) {
|
|||
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;
|
||||
|
@ -143,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]);
|
||||
|
@ -482,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);
|
||||
}
|
|
@ -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,6 +18,7 @@ namespace Y2RCommands {
|
|||
SetSendingY = 0x00100102,
|
||||
SetSendingU = 0x00110102,
|
||||
SetSendingV = 0x00120102,
|
||||
SetSendingYUV = 0x00130102,
|
||||
SetReceiving = 0x00180102,
|
||||
SetInputLineWidth = 0x001A0040,
|
||||
GetInputLineWidth = 0x001B0000,
|
||||
|
@ -82,6 +83,7 @@ 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;
|
||||
|
@ -399,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");
|
||||
|
|
|
@ -17,10 +17,11 @@ __declspec(dllexport) DWORD AmdPowerXpressRequestHighPerformance = 1;
|
|||
#endif
|
||||
|
||||
Emulator::Emulator()
|
||||
: config(getConfigPath()), 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
|
||||
|
@ -45,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();
|
||||
|
||||
|
@ -99,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]] {
|
||||
|
@ -117,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) {
|
||||
|
@ -127,26 +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__
|
||||
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
|
||||
|
||||
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);
|
||||
|
|
|
@ -134,25 +134,7 @@ void HydraCore::setPollInputCallback(void (*callback)()) { pollInputCallback = c
|
|||
void HydraCore::setCheckButtonCallback(s32 (*callback)(u32 player, hydra::ButtonType button)) { checkButtonCallback = callback; }
|
||||
|
||||
u32 HydraCore::addCheat(const u8* data, u32 size) {
|
||||
// Every 3DS cheat is a multiple of 64 bits == 8 bytes
|
||||
if ((size % 8) != 0) {
|
||||
return hydra::BAD_CHEAT;
|
||||
}
|
||||
|
||||
Cheats::Cheat cheat;
|
||||
cheat.enabled = true;
|
||||
cheat.type = Cheats::CheatType::ActionReplay;
|
||||
|
||||
for (u32 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 emulator->getCheats().addCheat(cheat);
|
||||
return emulator->getCheats().addCheat(data, size);
|
||||
};
|
||||
|
||||
void HydraCore::removeCheat(u32 id) { emulator->getCheats().removeCheat(id); }
|
||||
|
|
|
@ -35,6 +35,13 @@ JNIEnv* jniEnv() {
|
|||
|
||||
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(); }
|
||||
|
|
15
src/lua.cpp
15
src/lua.cpp
|
@ -1,6 +1,12 @@
|
|||
#ifdef PANDA3DS_ENABLE_LUA
|
||||
#include "lua_manager.hpp"
|
||||
|
||||
#ifndef __ANDROID__
|
||||
extern "C" {
|
||||
#include "luv.h"
|
||||
}
|
||||
#endif
|
||||
|
||||
void LuaManager::initialize() {
|
||||
L = luaL_newstate(); // Open Lua
|
||||
|
||||
|
@ -9,10 +15,15 @@ void LuaManager::initialize() {
|
|||
initialized = false;
|
||||
return;
|
||||
}
|
||||
|
||||
luaL_openlibs(L);
|
||||
initializeThunks();
|
||||
|
||||
#ifndef __ANDROID__
|
||||
lua_pushstring(L, "luv");
|
||||
luaopen_luv(L);
|
||||
lua_settable(L, LUA_GLOBALSINDEX);
|
||||
#endif
|
||||
|
||||
initializeThunks();
|
||||
initialized = true;
|
||||
haveScript = false;
|
||||
}
|
||||
|
|
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();
|
||||
}
|
|
@ -1,9 +1,14 @@
|
|||
#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");
|
||||
// Enable drop events for loading ROMs
|
||||
|
@ -26,8 +31,14 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent)
|
|||
// Create and bind actions for them
|
||||
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"));
|
||||
|
@ -40,20 +51,23 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent)
|
|||
|
||||
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);
|
||||
|
||||
auto aboutAction = aboutMenu->addAction(tr("About Panda3DS"));
|
||||
connect(aboutAction, &QAction::triggered, this, &MainWindow::showAboutMenu);
|
||||
|
||||
emu = new Emulator();
|
||||
emu->setOutputSize(screen.surfaceWidth, screen.surfaceHeight);
|
||||
|
||||
// Set up misc objects
|
||||
aboutWindow = new AboutWindow(nullptr);
|
||||
configWindow = new ConfigWindow(this);
|
||||
cheatsEditor = new CheatsWindow(emu, {}, this);
|
||||
luaEditor = new TextEditorWindow(this, "script.lua", "");
|
||||
|
||||
emu = new Emulator();
|
||||
emu->setOutputSize(screen.surfaceWidth, screen.surfaceHeight);
|
||||
|
||||
auto args = QCoreApplication::arguments();
|
||||
if (args.size() > 1) {
|
||||
auto romPath = std::filesystem::current_path() / args.at(1).toStdU16String();
|
||||
|
@ -176,6 +190,7 @@ MainWindow::~MainWindow() {
|
|||
delete menuBar;
|
||||
delete aboutWindow;
|
||||
delete configWindow;
|
||||
delete cheatsEditor;
|
||||
delete luaEditor;
|
||||
}
|
||||
|
||||
|
@ -194,8 +209,7 @@ 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();
|
||||
|
@ -226,6 +240,7 @@ void MainWindow::showAboutMenu() {
|
|||
}
|
||||
|
||||
void MainWindow::openLuaEditor() { luaEditor->show(); }
|
||||
void MainWindow::openCheatsEditor() { cheatsEditor->show(); }
|
||||
|
||||
void MainWindow::dispatchMessage(const EmulatorMessage& message) {
|
||||
switch (message.type) {
|
||||
|
@ -240,12 +255,33 @@ void MainWindow::dispatchMessage(const EmulatorMessage& message) {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -253,7 +289,12 @@ 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);
|
||||
};
|
||||
|
||||
|
@ -266,6 +307,11 @@ void MainWindow::keyPressEvent(QKeyEvent* event) {
|
|||
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;
|
||||
|
@ -282,7 +328,12 @@ 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);
|
||||
};
|
||||
|
||||
|
@ -295,6 +346,12 @@ void MainWindow::keyReleaseEvent(QKeyEvent* event) {
|
|||
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;
|
||||
|
@ -305,10 +362,56 @@ void MainWindow::keyReleaseEvent(QKeyEvent* event) {
|
|||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
|
@ -16,8 +16,8 @@ TextEditorWindow::TextEditorWindow(QWidget* parent, const std::string& filename,
|
|||
ZepReplExCommand::Register(zepWidget.GetEditor(), &replProvider);
|
||||
|
||||
// Default to standard mode instead of vim mode, initialize text box
|
||||
zepWidget.GetEditor().SetGlobalMode(Zep::ZepMode_Standard::StaticName());
|
||||
zepWidget.GetEditor().InitWithText(filename, initialText);
|
||||
zepWidget.GetEditor().SetGlobalMode(Zep::ZepMode_Standard::StaticName());
|
||||
|
||||
// Layout for widgets
|
||||
QVBoxLayout* mainLayout = new QVBoxLayout();
|
||||
|
@ -41,4 +41,4 @@ TextEditorWindow::TextEditorWindow(QWidget* parent, const std::string& filename,
|
|||
|
||||
mainLayout->addWidget(button);
|
||||
mainLayout->addWidget(&zepWidget);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,8 +21,20 @@ android {
|
|||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
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"
|
||||
|
@ -41,4 +53,4 @@ dependencies {
|
|||
implementation("androidx.preference:preference:1.2.1")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||
implementation("com.google.code.gson:gson:2.10.1")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,8 @@
|
|||
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
|
||||
|
@ -46,5 +48,7 @@
|
|||
|
||||
<activity android:name=".app.preferences.InputMapActivity"
|
||||
android:configChanges="density|orientation|screenSize"/>
|
||||
|
||||
<service android:name=".app.services.LoggerService" android:process=":logger_service"/>
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
|
@ -22,5 +22,7 @@ public class AlberDriver {
|
|||
public static native void LoadLuaScript(String script);
|
||||
public static native byte[] GetSmdh();
|
||||
|
||||
public static native void setShaderJitEnabled(boolean enable);
|
||||
|
||||
static { System.loadLibrary("Alber"); }
|
||||
}
|
|
@ -21,6 +21,7 @@ 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();
|
||||
|
@ -56,6 +57,11 @@ public class GameActivity extends BaseActivity {
|
|||
((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
|
||||
|
|
|
@ -2,11 +2,13 @@ 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;
|
||||
|
@ -24,6 +26,10 @@ public class PandroidApplication extends Application {
|
|||
GameUtils.initialize();
|
||||
InputMap.initialize();
|
||||
AlberDriver.Setup();
|
||||
|
||||
if (GlobalConfig.get(GlobalConfig.KEY_LOGGER_SERVICE)) {
|
||||
startService(new Intent(this, LoggerService.class));
|
||||
}
|
||||
}
|
||||
|
||||
public static int getThemeId() {
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
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;
|
||||
|
||||
|
||||
|
@ -15,4 +20,8 @@ public abstract class BasePreferenceFragment extends PreferenceFragmentCompat {
|
|||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
protected void setActivityTitle(@StringRes int titleId) {
|
||||
((AppCompatActivity) requireActivity()).getSupportActionBar().setTitle(titleId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ 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 {
|
||||
|
@ -16,5 +17,6 @@ public class SettingsFragment extends BasePreferenceFragment {
|
|||
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,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,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();
|
||||
}
|
||||
}
|
|
@ -5,7 +5,6 @@ import com.panda3ds.pandroid.data.GsonConfigParser;
|
|||
import com.panda3ds.pandroid.utils.Constants;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class GlobalConfig {
|
||||
|
@ -19,6 +18,9 @@ public class GlobalConfig {
|
|||
|
||||
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);
|
||||
|
||||
|
|
|
@ -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) {}
|
||||
}
|
||||
}
|
|
@ -5,6 +5,8 @@ public class Task extends Thread {
|
|||
super(runnable);
|
||||
}
|
||||
|
||||
protected Task() {}
|
||||
|
||||
public void runSync() {
|
||||
start();
|
||||
waitFinish();
|
||||
|
|
|
@ -70,6 +70,25 @@ public class FileUtils {
|
|||
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) {
|
||||
|
|
|
@ -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() {}
|
||||
}
|
|
@ -7,8 +7,10 @@ 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;
|
||||
|
@ -38,9 +40,12 @@ public class PandaGlRenderer implements GLSurfaceView.Renderer, ConsoleRenderer
|
|||
if (screenTexture != 0) {
|
||||
glDeleteTextures(1, new int[] {screenTexture}, 0);
|
||||
}
|
||||
if (screenFbo != 0) {
|
||||
|
||||
if (screenFbo != 0) {
|
||||
glDeleteFramebuffers(1, new int[] {screenFbo}, 0);
|
||||
}
|
||||
|
||||
PerformanceMonitor.destroy();
|
||||
super.finalize();
|
||||
}
|
||||
|
||||
|
@ -78,6 +83,7 @@ public class PandaGlRenderer implements GLSurfaceView.Renderer, ConsoleRenderer
|
|||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
|
||||
AlberDriver.Initialize();
|
||||
AlberDriver.setShaderJitEnabled(GlobalConfig.get(GlobalConfig.KEY_SHADER_JIT));
|
||||
AlberDriver.LoadRom(romPath);
|
||||
|
||||
// Load the SMDH
|
||||
|
@ -92,6 +98,8 @@ public class PandaGlRenderer implements GLSurfaceView.Renderer, ConsoleRenderer
|
|||
GameUtils.removeGame(game);
|
||||
GameUtils.addGame(GameMetadata.applySMDH(game, smdh));
|
||||
}
|
||||
|
||||
PerformanceMonitor.initialize(getBackendName());
|
||||
}
|
||||
|
||||
public void onDrawFrame(GL10 unused) {
|
||||
|
@ -114,6 +122,8 @@ public class PandaGlRenderer implements GLSurfaceView.Renderer, ConsoleRenderer
|
|||
screenHeight - bottomScreen.bottom, GL_COLOR_BUFFER_BIT, GL_LINEAR
|
||||
);
|
||||
}
|
||||
|
||||
PerformanceMonitor.runFrame();
|
||||
}
|
||||
|
||||
public void onSurfaceChanged(GL10 unused, int width, int height) {
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
package com.panda3ds.pandroid.view.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.text.Html;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
|
||||
import com.panda3ds.pandroid.data.config.GlobalConfig;
|
||||
import com.panda3ds.pandroid.utils.PerformanceMonitor;
|
||||
|
||||
public class PerformanceView extends AppCompatTextView {
|
||||
private boolean running = false;
|
||||
|
||||
public PerformanceView(@NonNull Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public PerformanceView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
this(context, attrs,0);
|
||||
}
|
||||
|
||||
public PerformanceView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
int padding = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, getResources().getDisplayMetrics());
|
||||
setPadding(padding,padding,padding,padding);
|
||||
setTextColor(Color.WHITE);
|
||||
setShadowLayer(padding,0,0,Color.BLACK);
|
||||
}
|
||||
|
||||
public void refresh(){
|
||||
running = isShown();
|
||||
if (!running) {
|
||||
return;
|
||||
}
|
||||
|
||||
String debug = "";
|
||||
|
||||
// Calculate total memory in MB and the current memory usage
|
||||
int memoryTotalMb = (int) Math.round(PerformanceMonitor.getTotalMemory() / (1024.0 * 1024.0));
|
||||
int memoryUsageMb = (int) Math.round(PerformanceMonitor.getUsedMemory() / (1024.0 * 1024.0));
|
||||
|
||||
debug += "<b>FPS: </b>" + PerformanceMonitor.getFps() + "<br>";
|
||||
debug += "<b>RAM: </b>" + Math.round(((float) memoryUsageMb / memoryTotalMb) * 100) + "% (" + memoryUsageMb + "MB/" + memoryTotalMb + "MB)<br>";
|
||||
debug += "<b>BACKEND: </b>" + PerformanceMonitor.getBackend() + (GlobalConfig.get(GlobalConfig.KEY_SHADER_JIT) ? " + JIT" : "") + "<br>";
|
||||
setText(Html.fromHtml(debug, Html.FROM_HTML_MODE_COMPACT));
|
||||
postDelayed(this::refresh, 250);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
if (!running) {
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -45,4 +45,13 @@
|
|||
<string name="open_file">Abrir arquivo</string>
|
||||
<string name="create_new">Criar novo</string>
|
||||
<string name="running_ff">Executando \"%s\" ...</string>
|
||||
</resources>
|
||||
<string name="developer_options">Opções de desenvolvedor</string>
|
||||
<string name="pref_developer_summary">Depuração, mostrar fps, etc.</string>
|
||||
<string name="pref_performance_monitor_title">Monitor de desempenho</string>
|
||||
<string name="pref_performance_monitor_summary">Mostrar um overlay com fps, memoria, etc.</string>
|
||||
<string name="pref_logger_service_title">Depuração</string>
|
||||
<string name="pref_logger_service_summary">Grave os registros para um arquivo.</string>
|
||||
<string name="pref_shader_jit_title">Shader Jit</string>
|
||||
<string name="pref_shader_jit_summary">Usar recompilador de shaders.</string>
|
||||
<string name="graphics">Gráficos</string>
|
||||
</resources>
|
||||
|
|
|
@ -46,4 +46,13 @@
|
|||
<string name="open_file">Open file</string>
|
||||
<string name="create_new">Create new</string>
|
||||
<string name="running_ff">Running \"%s\" ...</string>
|
||||
<string name="developer_options">Developer options</string>
|
||||
<string name="pref_developer_summary">Logger, FPS Counter, etc.</string>
|
||||
<string name="pref_performance_monitor_title">Performance monitor</string>
|
||||
<string name="pref_performance_monitor_summary">Show overlay with fps, memory, etc.</string>
|
||||
<string name="pref_logger_service_title">Logger</string>
|
||||
<string name="pref_logger_service_summary">Store application logs to file.</string>
|
||||
<string name="pref_shader_jit_title">Shader JIT</string>
|
||||
<string name="pref_shader_jit_summary">Use shader recompiler.</string>
|
||||
<string name="graphics">Graphics</string>
|
||||
</resources>
|
||||
|
|
|
@ -24,7 +24,11 @@
|
|||
<item name="android:textSize">32sp</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Pandroid" parent="Base.Theme.Pandroid" />
|
||||
<style name="Theme.Pandroid" parent="Base.Theme.Pandroid">
|
||||
<item name="android:enforceNavigationBarContrast">false</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
<item name="android:windowTranslucentNavigation">false</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Pandroid.Custom" parent="Theme.Pandroid">
|
||||
<item name="android:textColor">?colorOnSurface</item>
|
||||
|
@ -54,6 +58,7 @@
|
|||
<item name="android:textColorPrimary">@color/text_secondary_light</item>
|
||||
<item name="android:textColorSecondary">@color/text_secondary_light</item>
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
<item name="android:windowLightNavigationBar">false</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Pandroid.Black" parent="Theme.Pandroid.Custom">
|
||||
|
@ -72,6 +77,7 @@
|
|||
<item name="android:textColorPrimary">@color/text_secondary_light</item>
|
||||
<item name="android:textColorSecondary">@color/text_secondary_light</item>
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
<item name="android:windowLightNavigationBar">false</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Pandroid.Light" parent="Theme.Pandroid.Custom">
|
||||
|
@ -90,6 +96,7 @@
|
|||
<item name="android:windowLightStatusBar">true</item>
|
||||
<item name="android:textColorPrimary">@color/text_secondary_dark</item>
|
||||
<item name="android:textColorSecondary">@color/text_secondary_dark</item>
|
||||
<item name="android:windowLightNavigationBar">true</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
</resources>
|
||||
|
|
29
src/pandroid/app/src/main/res/xml/developer_preferences.xml
Normal file
29
src/pandroid/app/src/main/res/xml/developer_preferences.xml
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<SwitchPreference
|
||||
android:key="performanceMonitor"
|
||||
app:title="@string/pref_performance_monitor_title"
|
||||
app:summary="@string/pref_performance_monitor_summary"
|
||||
app:iconSpaceReserved="false"/>
|
||||
|
||||
<SwitchPreference
|
||||
android:key="loggerService"
|
||||
app:iconSpaceReserved="false"
|
||||
app:title="@string/pref_logger_service_title"
|
||||
android:summary="@string/pref_logger_service_summary"/>
|
||||
|
||||
<PreferenceCategory
|
||||
app:iconSpaceReserved="false"
|
||||
app:title="@string/graphics">
|
||||
|
||||
<SwitchPreference
|
||||
app:key="shaderJit"
|
||||
app:title="@string/pref_shader_jit_title"
|
||||
app:summary="@string/pref_shader_jit_summary"
|
||||
app:iconSpaceReserved="false"/>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
|
@ -23,4 +23,11 @@
|
|||
app:summary="@string/pref_appearance_summary"
|
||||
app:layout="@layout/preference_start_item"/>
|
||||
|
||||
<Preference
|
||||
app:key="developer"
|
||||
app:icon="@drawable/ic_code"
|
||||
app:title="@string/developer_options"
|
||||
app:summary="@string/pref_developer_summary"
|
||||
app:layout="@layout/preference_start_item"/>
|
||||
|
||||
</PreferenceScreen>
|
Loading…
Add table
Add a link
Reference in a new issue