mirror of
https://github.com/wheremyfoodat/Panda3DS.git
synced 2025-04-20 12:39:13 +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");
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue