Merge branch 'master' into mii_selector

This commit is contained in:
wheremyfoodat 2023-08-21 18:53:36 +03:00
commit 3b22f15e67
103 changed files with 2118 additions and 262550 deletions

View file

@ -1,5 +1,6 @@
#include "config.hpp"
#include <cmath>
#include <fstream>
#include <string>
@ -54,7 +55,30 @@ void EmulatorConfig::load(const std::filesystem::path& path) {
rendererType = RendererType::OpenGL;
}
shaderJitEnabled = toml::find_or<toml::boolean>(gpu, "EnableShaderJIT", false);
shaderJitEnabled = toml::find_or<toml::boolean>(gpu, "EnableShaderJIT", true);
}
}
if (data.contains("Battery")) {
auto batteryResult = toml::expect<toml::value>(data.at("Battery"));
if (batteryResult.is_ok()) {
auto battery = batteryResult.unwrap();
chargerPlugged = toml::find_or<toml::boolean>(battery, "ChargerPlugged", true);
batteryPercentage = toml::find_or<toml::integer>(battery, "BatteryPercentage", 3);
// Clamp battery % to [0, 100] to make sure it's a valid value
batteryPercentage = std::clamp(batteryPercentage, 0, 100);
}
}
if (data.contains("SD")) {
auto sdResult = toml::expect<toml::value>(data.at("SD"));
if (sdResult.is_ok()) {
auto sd = sdResult.unwrap();
sdCardInserted = toml::find_or<toml::boolean>(sd, "UseVirtualSD", true);
sdWriteProtected = toml::find_or<toml::boolean>(sd, "WriteProtectVirtualSD", false);
}
}
}
@ -81,6 +105,12 @@ void EmulatorConfig::save(const std::filesystem::path& path) {
data["GPU"]["EnableShaderJIT"] = shaderJitEnabled;
data["GPU"]["Renderer"] = std::string(Renderer::typeToString(rendererType));
data["Battery"]["ChargerPlugged"] = chargerPlugged;
data["Battery"]["BatteryPercentage"] = batteryPercentage;
data["SD"]["UseVirtualSD"] = sdCardInserted;
data["SD"]["WriteProtectVirtualSD"] = sdWriteProtected;
std::ofstream file(path, std::ios::out);
file << data;
file.close();

View file

@ -143,7 +143,9 @@ void ShaderEmitter::compileInstruction(const PICAShader& shaderUnit) {
break;
case ShaderOpcodes::DP3: recDP3(shaderUnit, instruction); break;
case ShaderOpcodes::DP4: recDP4(shaderUnit, instruction); break;
case ShaderOpcodes::DPH: recDPH(shaderUnit, instruction); break;
case ShaderOpcodes::DPH:
case ShaderOpcodes::DPHI:
recDPH(shaderUnit, instruction); break;
case ShaderOpcodes::END: recEND(shaderUnit, instruction); break;
case ShaderOpcodes::EX2: recEX2(shaderUnit, instruction); break;
case ShaderOpcodes::FLR: recFLR(shaderUnit, instruction); break;
@ -531,15 +533,17 @@ void ShaderEmitter::recDP4(const PICAShader& shader, u32 instruction) {
}
void ShaderEmitter::recDPH(const PICAShader& shader, u32 instruction) {
const bool isDPHI = (instruction >> 26) == ShaderOpcodes::DPHI;
const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f];
const u32 src1 = getBits<12, 7>(instruction);
const u32 src2 = getBits<7, 5>(instruction); // src2 coming first because PICA moment
const u32 src1 = isDPHI ? getBits<14, 5>(instruction) : getBits<12, 7>(instruction);
const u32 src2 = isDPHI ? getBits<7, 7>(instruction) : getBits<7, 5>(instruction);
const u32 idx = getBits<19, 2>(instruction);
const u32 dest = getBits<21, 5>(instruction);
// TODO: Safe multiplication equivalent (Multiplication is not IEEE compliant on the PICA)
loadRegister<1>(src1_xmm, shader, src1, idx, operandDescriptor);
loadRegister<2>(src2_xmm, shader, src2, 0, operandDescriptor);
loadRegister<1>(src1_xmm, shader, src1, isDPHI ? 0 : idx, operandDescriptor);
loadRegister<2>(src2_xmm, shader, src2, isDPHI ? idx : 0, operandDescriptor);
// Attach 1.0 to the w component of src1
if (haveSSE4_1) {

View file

@ -16,6 +16,12 @@
#include "renderer_vk/renderer_vk.hpp"
#endif
constexpr u32 topScreenWidth = 240;
constexpr u32 topScreenHeight = 400;
constexpr u32 bottomScreenWidth = 240;
constexpr u32 bottomScreenHeight = 300;
using namespace Floats;
// Note: For when we have multiple backends, the GL state manager can stay here and have the constructor for the Vulkan-or-whatever renderer ignore it
@ -26,24 +32,24 @@ GPU::GPU(Memory& mem, EmulatorConfig& config) : mem(mem), config(config) {
switch (config.rendererType) {
case RendererType::Null: {
renderer.reset(new RendererNull(*this, regs));
renderer.reset(new RendererNull(*this, regs, externalRegs));
break;
}
case RendererType::Software: {
renderer.reset(new RendererSw(*this, regs));
renderer.reset(new RendererSw(*this, regs, externalRegs));
break;
}
#ifdef PANDA3DS_ENABLE_OPENGL
case RendererType::OpenGL: {
renderer.reset(new RendererGL(*this, regs));
renderer.reset(new RendererGL(*this, regs, externalRegs));
break;
}
#endif
#ifdef PANDA3DS_ENABLE_VULKAN
case RendererType::Vulkan: {
renderer.reset(new RendererVK(*this, regs));
renderer.reset(new RendererVK(*this, regs, externalRegs));
break;
}
#endif
@ -78,6 +84,27 @@ void GPU::reset() {
e.config2 = 0;
}
// Initialize the framebuffer registers. Values taken from Citra.
using namespace PICA::ExternalRegs;
// Top screen addresses and dimentions.
externalRegs[Framebuffer0AFirstAddr] = 0x181E6000;
externalRegs[Framebuffer0ASecondAddr] = 0x1822C800;
externalRegs[Framebuffer0BFirstAddr] = 0x18273000;
externalRegs[Framebuffer0BSecondAddr] = 0x182B9800;
externalRegs[Framebuffer0Size] = (topScreenHeight << 16) | topScreenWidth;
externalRegs[Framebuffer0Stride] = 720;
externalRegs[Framebuffer0Config] = static_cast<u32>(PICA::ColorFmt::RGB8);
externalRegs[Framebuffer0Select] = 0;
// Bottom screen addresses and dimentions.
externalRegs[Framebuffer1AFirstAddr] = 0x1848F000;
externalRegs[Framebuffer1ASecondAddr] = 0x184C7800;
externalRegs[Framebuffer1Size] = (bottomScreenHeight << 16) | bottomScreenWidth;
externalRegs[Framebuffer1Stride] = 720;
externalRegs[Framebuffer1Config] = static_cast<u32>(PICA::ColorFmt::RGB8);
externalRegs[Framebuffer1Select] = 0;
renderer->reset();
}
@ -321,15 +348,17 @@ PICA::Vertex GPU::getImmediateModeVertex() {
// Run VS and return vertex data. TODO: Don't hardcode offsets for each attribute
shaderUnit.vs.run();
std::memcpy(&v.s.positions, &shaderUnit.vs.outputs[0], sizeof(vec4f));
std::memcpy(&v.s.colour, &shaderUnit.vs.outputs[1], sizeof(vec4f));
std::memcpy(&v.s.texcoord0, &shaderUnit.vs.outputs[2], 2 * sizeof(f24));
// Map shader outputs to fixed function properties
const u32 totalShaderOutputs = regs[PICA::InternalRegs::ShaderOutputCount] & 7;
for (int i = 0; i < totalShaderOutputs; i++) {
const u32 config = regs[PICA::InternalRegs::ShaderOutmap0 + i];
printf(
"(x, y, z, w) = (%f, %f, %f, %f)\n", (double)v.s.positions[0], (double)v.s.positions[1], (double)v.s.positions[2], (double)v.s.positions[3]
);
printf("(r, g, b, a) = (%f, %f, %f, %f)\n", (double)v.s.colour[0], (double)v.s.colour[1], (double)v.s.colour[2], (double)v.s.colour[3]);
printf("(u, v ) = (%f, %f)\n", (double)v.s.texcoord0[0], (double)v.s.texcoord0[1]);
for (int j = 0; j < 4; j++) { // pls unroll
const u32 mapping = (config >> (j * 8)) & 0x1F;
v.raw[mapping] = shaderUnit.vs.outputs[i][j];
}
}
return v;
}

View file

@ -19,11 +19,36 @@ void GPU::writeReg(u32 address, u32 value) {
if (address >= 0x1EF01000 && address < 0x1EF01C00) { // Internal registers
const u32 index = (address - 0x1EF01000) / sizeof(u32);
writeInternalReg(index, value, 0xffffffff);
} else if (address >= 0x1EF00004 && address < 0x1EF01000) {
const u32 index = (address - 0x1EF00004) / sizeof(u32);
writeExternalReg(index, value);
} else {
log("Ignoring write to external GPU register %08X. Value: %08X\n", address, value);
log("Ignoring write to unknown GPU register %08X. Value: %08X\n", address, value);
}
}
u32 GPU::readExternalReg(u32 index) {
using namespace PICA::ExternalRegs;
if (index > 0x1000) [[unlikely]] {
Helpers::panic("Tried to read invalid external GPU register. Index: %X\n", index);
return -1;
}
return externalRegs[index];
}
void GPU::writeExternalReg(u32 index, u32 value) {
using namespace PICA::ExternalRegs;
if (index > 0x1000) [[unlikely]] {
Helpers::panic("Tried to write to invalid external GPU register. Index: %X, value: %08X\n", index, value);
return;
}
externalRegs[index] = value;
}
u32 GPU::readInternalReg(u32 index) {
using namespace PICA::InternalRegs;
@ -384,4 +409,4 @@ void GPU::startCommandList(u32 addr, u32 size) {
writeInternalReg(id, param, mask);
}
}
}
}

View file

@ -45,6 +45,7 @@ void PICAShader::run() {
case ShaderOpcodes::NOP: break; // Do nothing
case ShaderOpcodes::RCP: rcp(instruction); break;
case ShaderOpcodes::RSQ: rsq(instruction); break;
case ShaderOpcodes::SGE: sge(instruction); break;
case ShaderOpcodes::SGEI: sgei(instruction); break;
case ShaderOpcodes::SLT: slt(instruction); break;
case ShaderOpcodes::SLTI: slti(instruction); break;
@ -517,6 +518,26 @@ void PICAShader::slt(u32 instruction) {
}
}
void PICAShader::sge(u32 instruction) {
const u32 operandDescriptor = operandDescriptors[instruction & 0x7f];
u32 src1 = getBits<12, 7>(instruction);
const u32 src2 = getBits<7, 5>(instruction); // src2 coming first because PICA moment
const u32 idx = getBits<19, 2>(instruction);
const u32 dest = getBits<21, 5>(instruction);
src1 = getIndexedSource(src1, idx);
vec4f srcVec1 = getSourceSwizzled<1>(src1, operandDescriptor);
vec4f srcVec2 = getSourceSwizzled<2>(src2, operandDescriptor);
auto& destVector = getDest(dest);
u32 componentMask = operandDescriptor & 0xf;
for (int i = 0; i < 4; i++) {
if (componentMask & (1 << i)) {
destVector[3 - i] = srcVec1[3 - i] >= srcVec2[3 - i] ? f24::fromFloat32(1.0) : f24::zero();
}
}
}
void PICAShader::sgei(u32 instruction) {
const u32 operandDescriptor = operandDescriptors[instruction & 0x7f];
const u32 src1 = getBits<14, 5>(instruction);

View file

@ -20,9 +20,11 @@ HorizonResult ExtSaveDataArchive::createFile(const FSPath& path, u64 size) {
// Create a file of size "size" by creating an empty one, seeking to size - 1 and just writing a 0 there
IOFile file(p.string().c_str(), "wb");
if (file.seek(size - 1, SEEK_SET) && file.writeBytes("", 1).second == 1) {
file.close();
return Result::Success;
}
file.close();
return Result::FS::FileTooLarge;
}

View file

@ -19,14 +19,17 @@ HorizonResult SaveDataArchive::createFile(const FSPath& path, u64 size) {
// If the size is 0, leave the file empty and return success
if (size == 0) {
file.close();
return Result::Success;
}
// If it is not empty, seek to size - 1 and write a 0 to create a file of size "size"
else if (file.seek(size - 1, SEEK_SET) && file.writeBytes("", 1).second == 1) {
file.close();
return Result::Success;
}
file.close();
return Result::FS::FileTooLarge;
}

View file

@ -5,7 +5,8 @@
namespace PathType {
enum : u32 {
RomFS = 0,
ExeFS = 2
ExeFS = 2,
UpdateRomFS = 5,
};
};
@ -33,8 +34,8 @@ FileDescriptor SelfNCCHArchive::openFile(const FSPath& path, const FilePerms& pe
// Where to read the file from. (https://www.3dbrew.org/wiki/Filesystem_services#SelfNCCH_File_Path_Data_Format)
// We currently only know how to read from an NCCH's RomFS, ie type = 0
const u32 type = *(u32*)&path.binary[0]; // TODO: Get rid of UB here
if (type != PathType::RomFS && type != PathType::ExeFS) {
Helpers::panic("Read from NCCH's non-RomFS & non-exeFS section!");
if (type != PathType::RomFS && type != PathType::ExeFS && type != PathType::UpdateRomFS) {
Helpers::panic("Read from NCCH's non-RomFS & non-exeFS section! Path type: %d", type);
}
return NoFile; // No file descriptor needed for RomFS
@ -50,8 +51,8 @@ Rust::Result<ArchiveBase*, HorizonResult> SelfNCCHArchive::openArchive(const FSP
}
std::optional<u32> SelfNCCHArchive::readFile(FileSession* file, u64 offset, u32 size, u32 dataPointer) {
const FSPath& path = file->path; // Path of the file
const u32 type = *(u32*)&path.binary[0]; // Type of the path
const FSPath& path = file->path; // Path of the file
const u32 type = *(u32*)&path.binary[0]; // Type of the path
if (type == PathType::RomFS && !hasRomFS()) {
Helpers::panic("Tried to read file from non-existent RomFS");
@ -98,8 +99,23 @@ std::optional<u32> SelfNCCHArchive::readFile(FileSession* file, u64 offset, u32
break;
}
default:
Helpers::panic("Unimplemented file path type for SelfNCCH archive");
// Normally, the update RomFS should overlay the cartridge RomFS when reading from this and an update is installed.
// So to support updates, we need to perform this overlaying. For now, read from the cartridge RomFS.
case PathType::UpdateRomFS: {
Helpers::warn("Reading from update RomFS but updates are currently not supported! Reading from regular RomFS instead\n");
const u64 romFSSize = cxi->romFS.size;
const u64 romFSOffset = cxi->romFS.offset;
if ((offset >> 32) || (offset >= romFSSize) || (offset + size >= romFSSize)) {
Helpers::panic("Tried to read from SelfNCCH with too big of an offset");
}
fsInfo = cxi->romFS;
offset += 0x1000;
break;
}
default: Helpers::panic("Unimplemented file path type for SelfNCCH archive");
}
std::unique_ptr<u8[]> data(new u8[size]);

View file

@ -58,7 +58,7 @@ namespace IVFC {
// According to 3DBrew, this is usually the case but not guaranteed
if (ivfcActualSize != ivfcDescriptorSize) {
printf("IVFC descriptor size mismatch: %lx != %lx\n", ivfcActualSize, ivfcDescriptorSize);
printf("IVFC descriptor size mismatch: %llx != %llx\n", ivfcActualSize, ivfcDescriptorSize);
}
if (magicIdentifier == 0x10000 && ivfcActualSize != 0x5C) {
@ -73,4 +73,4 @@ namespace IVFC {
return ivfcActualSize;
}
} // namespace IVFC
} // namespace IVFC

View file

@ -3,9 +3,10 @@
#include "kernel_types.hpp"
#include "cpu.hpp"
Kernel::Kernel(CPU& cpu, Memory& mem, GPU& gpu)
: cpu(cpu), regs(cpu.regs()), mem(mem), handleCounter(0), serviceManager(regs, mem, gpu, currentProcess, *this) {
Kernel::Kernel(CPU& cpu, Memory& mem, GPU& gpu, const EmulatorConfig& config)
: cpu(cpu), regs(cpu.regs()), mem(mem), handleCounter(0), serviceManager(regs, mem, gpu, currentProcess, *this, config) {
objects.reserve(512); // Make room for a few objects to avoid further memory allocs later
mutexHandles.reserve(8);
portHandles.reserve(32);
threadIndices.reserve(appResourceLimits.maxThreads);
@ -34,6 +35,8 @@ void Kernel::serviceSVC(u32 svc) {
case 0x0A: svcSleepThread(); break;
case 0x0B: getThreadPriority(); break;
case 0x0C: setThreadPriority(); break;
case 0x0F: getThreadIdealProcessor(); break;
case 0x11: getCurrentProcessorNumber(); break;
case 0x13: svcCreateMutex(); break;
case 0x14: svcReleaseMutex(); break;
case 0x15: svcCreateSemaphore(); break;
@ -41,6 +44,10 @@ void Kernel::serviceSVC(u32 svc) {
case 0x17: svcCreateEvent(); break;
case 0x18: svcSignalEvent(); break;
case 0x19: svcClearEvent(); break;
case 0x1A: svcCreateTimer(); break;
case 0x1B: svcSetTimer(); break;
case 0x1C: svcCancelTimer(); break;
case 0x1D: svcClearTimer(); break;
case 0x1E: createMemoryBlock(); break;
case 0x1F: mapMemoryBlock(); break;
case 0x21: createAddressArbiter(); break;
@ -139,6 +146,7 @@ void Kernel::reset() {
deleteObjectData(object);
}
objects.clear();
mutexHandles.clear();
portHandles.clear();
threadIndices.clear();
serviceManager.reset();
@ -152,7 +160,7 @@ void Kernel::reset() {
// Make main thread object. We do not have to set the entrypoint and SP for it as the ROM loader does.
// Main thread seems to have a priority of 0x30. TODO: This creates a dummy context for thread 0,
// which is thankfully not used. Maybe we should prevent this
mainThread = makeThread(0, VirtualAddrs::StackTop, 0x30, -2, 0, ThreadStatus::Running);
mainThread = makeThread(0, VirtualAddrs::StackTop, 0x30, ProcessorID::Default, 0, ThreadStatus::Running);
currentThreadIndex = 0;
setupIdleThread();
@ -256,6 +264,7 @@ void Kernel::duplicateHandle() {
namespace SystemInfoType {
enum : u32 {
MemoryInformation = 0,
// Gets information related to Citra (We don't implement this, we just report this emulator is not Citra)
CitraInformation = 0x20000,
// Gets information related to this emulator
@ -292,6 +301,22 @@ void Kernel::getSystemInfo() {
regs[0] = Result::Success;
switch (infoType) {
case SystemInfoType::MemoryInformation: {
switch (subtype) {
// Total used memory size in the APPLICATION memory region
case 1:
regs[1] = mem.getUsedUserMem();
regs[2] = 0;
break;
default:
Helpers::panic("GetSystemInfo: Unknown MemoryInformation subtype %x\n", subtype);
regs[0] = Result::FailurePlaceholder;
break;
}
break;
}
case SystemInfoType::CitraInformation: {
switch (subtype) {
case CitraInfoType::IsCitra:

View file

@ -1,5 +1,4 @@
#include "kernel.hpp"
#include "services/shared_font.hpp"
namespace Operation {
enum : u32 {
@ -137,7 +136,7 @@ void Kernel::mapMemoryBlock() {
break;
case KernelHandles::FontSharedMemHandle:
std::memcpy(ptr, _shared_font_bin, _shared_font_len);
mem.copySharedFont(ptr);
break;
default: Helpers::panic("Mapping unknown shared memory block: %X", block);

View file

@ -109,7 +109,7 @@ void Kernel::sendSyncRequest() {
// If we're actually communicating with a port
const auto session = getObject(handle, KernelObjectType::Session);
if (session == nullptr) [[unlikely]] {
Helpers::panic("SendSyncRequest: Invalid handle");
Helpers::warn("SendSyncRequest: Invalid handle");
regs[0] = Result::Kernel::InvalidHandle;
return;
}
@ -127,4 +127,4 @@ void Kernel::sendSyncRequest() {
const auto portData = objects[portHandle].getData<Port>();
Helpers::panic("SendSyncRequest targetting port %s\n", portData->name);
}
}
}

View file

@ -89,6 +89,7 @@ s32 Kernel::getCurrentResourceValue(const KernelObject* limit, u32 resourceName)
u32 Kernel::getMaxForResource(const KernelObject* limit, u32 resourceName) {
switch (resourceName) {
case ResourceType::Commit: return appResourceLimits.maxCommit;
case ResourceType::Thread: return appResourceLimits.maxThreads;
default: Helpers::panic("Attempted to get the max of unknown kernel resource: %d\n", resourceName);
}
}
}

View file

@ -106,7 +106,7 @@ void Kernel::rescheduleThreads() {
}
// Internal OS function to spawn a thread
Handle Kernel::makeThread(u32 entrypoint, u32 initialSP, u32 priority, s32 id, u32 arg, ThreadStatus status) {
Handle Kernel::makeThread(u32 entrypoint, u32 initialSP, u32 priority, ProcessorID id, u32 arg, ThreadStatus status) {
int index; // Index of the created thread in the threads array
if (threadCount < appResourceLimits.maxThreads) [[likely]] { // If we have not yet created over too many threads
@ -168,6 +168,11 @@ Handle Kernel::makeMutex(bool locked) {
moo->ownerThread = currentThreadIndex;
}
// Push the new mutex to our list of mutex handles
// We need a list of mutex handles so that when a thread is killed, we can look which mutexes from this list the thread owns and free them
// Alternatively this could be a per-thread list, but I don't want to push_back and remove on every mutex lock and release
// Since some mutexes like the APT service mutex are locked and unlocked constantly, while ExitThread is a relatively "rare" SVC
mutexHandles.push_back(ret);
return ret;
}
@ -384,8 +389,12 @@ void Kernel::createThread() {
return;
}
if (id < -2 || id > 3) {
Helpers::panic("Invalid processor ID in CreateThread");
}
regs[0] = Result::Success;
regs[1] = makeThread(entrypoint, initialSP, priority, id, arg, ThreadStatus::Ready);
regs[1] = makeThread(entrypoint, initialSP, priority, static_cast<ProcessorID>(id), arg, ThreadStatus::Ready);
requireReschedule();
}
@ -436,6 +445,15 @@ void Kernel::getThreadPriority() {
}
}
void Kernel::getThreadIdealProcessor() {
const Handle handle = regs[1]; // Thread handle
logSVC("GetThreadIdealProcessor (handle = %X)\n", handle);
// TODO: Not documented what this is or what it does. Citra doesn't implement it at all. Return AppCore as the ideal processor for now
regs[0] = Result::Success;
regs[1] = static_cast<u32>(ProcessorID::AppCore);
}
void Kernel::setThreadPriority() {
const Handle handle = regs[0];
const u32 priority = regs[1];
@ -463,9 +481,53 @@ void Kernel::setThreadPriority() {
requireReschedule();
}
void Kernel::getCurrentProcessorNumber() {
logSVC("GetCurrentProcessorNumber()\n");
const ProcessorID id = threads[currentThreadIndex].processorID;
s32 ret;
// Until we properly implement per-core schedulers, return whatever processor ID passed to svcCreateThread
switch (id) {
// TODO: This is picked from exheader
case ProcessorID::Default:
ret = static_cast<s32>(ProcessorID::AppCore);
break;
case ProcessorID::AllCPUs:
ret = static_cast<s32>(ProcessorID::AppCore);
Helpers::warn("GetCurrentProcessorNumber on thread created to run on all CPUs...?\n");
break;
default: ret = static_cast<s32>(id); break;
}
if (ret != static_cast<s32>(ProcessorID::AppCore)) {
Helpers::warn("GetCurrentProcessorNumber: Thread not running on appcore\n");
}
regs[0] = static_cast<u32>(ret);
}
void Kernel::exitThread() {
logSVC("ExitThread\n");
// Find which mutexes this thread owns, release them
for (auto handle : mutexHandles) {
KernelObject* object = getObject(handle, KernelObjectType::Mutex);
// Make sure that the handle actually matches to a mutex, and if our exiting thread owns the mutex, release it
if (object != nullptr) {
Mutex* moo = object->getData<Mutex>();
if (moo->locked && moo->ownerThread == currentThreadIndex) {
// Release the mutex by setting lock count to 1 and releasing it once. We set lock count to 1 since it's a recursive mutex
// Therefore if its lock count was > 1, simply calling releaseMutex would not fully release it
moo->lockCount = 1;
releaseMutex(moo);
}
}
}
// Remove the index of this thread from the thread indices vector
for (int i = 0; i < threadIndices.size(); i++) {
if (threadIndices[i] == currentThreadIndex)
@ -597,4 +659,4 @@ bool Kernel::shouldWaitOnObject(KernelObject* object) {
Helpers::panic("Not sure whether to wait on object (type: %s)", object->getTypeName());
return true;
}
}
}

View file

@ -0,0 +1,6 @@
#include "kernel.hpp"
void Kernel::svcCreateTimer() { Helpers::panic("Kernel::CreateTimer"); }
void Kernel::svcSetTimer() { Helpers::panic("Kernel::SetTimer"); }
void Kernel::svcClearTimer() { Helpers::panic("Kernel::ClearTimer"); }
void Kernel::svcCancelTimer() { Helpers::panic("Kernel::CancelTimer"); }

View file

@ -63,5 +63,7 @@ std::optional<u32> Memory::loadELF(std::ifstream& file) {
allocateMemory(vaddr, fcramAddr, memorySize, true, r, w, x);
}
// ELF can't specify a region, make it default to USA
region = Regions::USA;
return static_cast<u32>(reader.get_entry());
}

View file

@ -131,6 +131,13 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn
return false;
}
if (!aesEngine.haveGenerator()) {
Helpers::panic(
"Loading an encrypted ROM but your AES keys don't seem to provide the \"generator\" constant which Panda3DS requires for decryption\n"
"Please add it to your aes_keys.txt in a line like \"generator=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\" where the Xs are replaced with the actual generator constant value"
);
}
if (!gotCryptoKeys) {
Helpers::panic("ROM is encrypted but it seems we couldn't get either the primary or the secondary key");
return false;
@ -209,10 +216,25 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn
codeFile.resize(fileSize);
readFromFile(file, exeFS, codeFile.data(), fileOffset + exeFSHeaderSize, fileSize);
}
} else if (std::strcmp(name, "icon") == 0) {
// Parse icon file to extract region info and more in the future (logo, etc)
std::vector<u8> tmp;
tmp.resize(fileSize);
readFromFile(file, exeFS, tmp.data(), fileOffset + exeFSHeaderSize, fileSize);
if (!parseSMDH(tmp)) {
printf("Failed to parse SMDH!\n");
}
}
}
}
// If no region has been detected for CXI, set the region to USA by default
if (!region.has_value() && partitionIndex == 0) {
printf("No region detected for CXI, defaulting to USA\n");
region = Regions::USA;
}
if (hasRomFS()) {
printf("RomFS offset: %08llX, size: %08llX\n", romFS.offset, romFS.size);
}
@ -221,6 +243,52 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn
return true;
}
bool NCCH::parseSMDH(const std::vector<u8>& smdh) {
if (smdh.size() < 0x36C0) {
printf("The cartridge .icon file is too small, considered invalid. Must be 0x36C0 bytes minimum\n");
return false;
}
if (char(smdh[0]) != 'S' || char(smdh[1]) != 'M' || char(smdh[2]) != 'D' || char(smdh[3]) != 'H') {
printf("Invalid SMDH magic!\n");
return false;
}
// Bitmask showing which regions are allowed.
// https://www.3dbrew.org/wiki/SMDH#Region_Lockout
const u32 regionMasks = *(u32*)&smdh[0x2018];
// Detect when games are region free (ie all regions are allowed) for future use
[[maybe_unused]] const bool isRegionFree = (regionMasks & 0x7f) == 0x7f;
// See which countries are allowed
const bool japan = (regionMasks & 0x1) != 0;
const bool northAmerica = (regionMasks & 0x2) != 0;
const bool europe = (regionMasks & 0x4) != 0;
const bool australia = (regionMasks & 0x8) != 0;
const bool china = (regionMasks & 0x10) != 0;
const bool korea = (regionMasks & 0x20) != 0;
const bool taiwan = (regionMasks & 0x40) != 0;
// Based on the allowed regions, set the autodetected 3DS region. We currently prefer English-speaking regions for practical purposes.
// But this should be configurable later.
if (northAmerica) {
region = Regions::USA;
} else if (europe) {
region = Regions::Europe;
} else if (australia) {
region = Regions::Australia;
} else if (japan) {
region = Regions::Japan;
} else if (korea) {
region = Regions::Korea;
} else if (china) {
region = Regions::China;
} else if (taiwan) {
region = Regions::Taiwan;
}
return true;
}
std::pair<bool, Crypto::AESKey> NCCH::getPrimaryKey(Crypto::AESEngine &aesEngine, const Crypto::AESKey &keyY) {
Crypto::AESKey result;

View file

@ -11,6 +11,12 @@ bool Memory::mapCXI(NCSD& ncsd, NCCH& cxi) {
printf("Data address = %08X, size = %08X\n", cxi.data.address, cxi.data.size);
printf("Stack size: %08X\n", cxi.stackSize);
static constexpr std::array<const char*, 7> regionNames = {"Japan", "North America", "Europe", "Australia", "China", "Korea", "Taiwan" };
// Set autodetected 3DS region to one of the values allowed by the CXI's SMDH
region = cxi.region.value();
printf("Console region autodetected to: %s\n", regionNames[static_cast<size_t>(region)]);
if (!isAligned(cxi.stackSize)) {
Helpers::warn("CXI has a suspicious stack size of %08X which is not a multiple of 4KB", cxi.stackSize);
}

View file

@ -2,14 +2,18 @@
#include <cassert>
#include <chrono> // For time since epoch
#include <cmrc/cmrc.hpp>
#include <ctime>
#include "config_mem.hpp"
#include "resource_limits.hpp"
#include "services/ptm.hpp"
CMRC_DECLARE(ConsoleFonts);
using namespace KernelMemoryTypes;
Memory::Memory(u64& cpuTicks) : cpuTicks(cpuTicks) {
Memory::Memory(u64& cpuTicks, const EmulatorConfig& config) : cpuTicks(cpuTicks), config(config) {
fcram = new uint8_t[FCRAM_SIZE]();
dspRam = new uint8_t[DSP_RAM_SIZE]();
@ -45,6 +49,12 @@ void Memory::reset() {
// Initialize shared memory blocks and reserve memory for them
for (auto& e : sharedMemBlocks) {
if (e.handle == KernelHandles::FontSharedMemHandle) {
// Read font size from the cmrc filesystem the font is stored in
auto fonts = cmrc::ConsoleFonts::get_filesystem();
e.size = fonts.open("CitraSharedFontUSRelocated.bin").size();
}
e.mapped = false;
e.paddr = allocateSysMemory(e.size);
}
@ -59,6 +69,9 @@ void Memory::reset() {
readTable[i + initialPage] = pointer;
writeTable[i + initialPage] = pointer;
}
// Later adjusted based on ROM header when possible
region = Regions::USA;
}
bool Memory::allocateMainThreadStack(u32 size) {
@ -82,7 +95,18 @@ u8 Memory::read8(u32 vaddr) {
return *(u8*)(pointer + offset);
} else {
switch (vaddr) {
case ConfigMem::BatteryState: return getBatteryState(true, true, BatteryLevel::FourBars);
case ConfigMem::BatteryState: {
// Set by the PTM module
// Charger plugged: Shows whether the charger is plugged
// Charging: Shows whether the charger is plugged and the console is actually charging, ie the battery is not full
// BatteryLevel: A battery level calculated via PTM::GetBatteryLevel
// These are all assembled into a bitfield and returned via config memory
const bool chargerPlugged = config.chargerPlugged;
const bool charging = config.chargerPlugged && (config.batteryPercentage < 100);
const auto batteryLevel = static_cast<BatteryLevel>(PTMService::batteryPercentToLevel(config.batteryPercentage));
return getBatteryState(chargerPlugged, charging, batteryLevel);
}
case ConfigMem::EnvInfo: return envInfo;
case ConfigMem::HardwareType: return ConfigMem::HardwareCodes::Product;
case ConfigMem::KernelVersionMinor: return u8(kernelVersion & 0xff);
@ -91,6 +115,12 @@ u8 Memory::read8(u32 vaddr) {
case ConfigMem::NetworkState: return 2; // Report that we've got an internet connection
case ConfigMem::HeadphonesConnectedMaybe: return 0;
case ConfigMem::Unknown1086: return 1; // It's unknown what this is but some games want it to be 1
case ConfigMem::FirmUnknown: return firm.unk;
case ConfigMem::FirmRevision: return firm.revision;
case ConfigMem::FirmVersionMinor: return firm.minor;
case ConfigMem::FirmVersionMajor: return firm.major;
default: Helpers::panic("Unimplemented 8-bit read, addr: %08X", vaddr);
}
}
@ -138,11 +168,19 @@ u32 Memory::read32(u32 vaddr) {
// 3D slider. Float in range 0.0 = off, 1.0 = max.
case ConfigMem::SliderState3D: return Helpers::bit_cast<u32, float>(0.0f);
case ConfigMem::FirmUnknown:
return u32(read8(vaddr)) | (u32(read8(vaddr + 1)) << 8) | (u32(read8(vaddr + 2)) << 16) | (u32(read8(vaddr + 3)) << 24);
default:
if (vaddr >= VirtualAddrs::VramStart && vaddr < VirtualAddrs::VramStart + VirtualAddrs::VramSize) {
Helpers::warn("VRAM read!\n");
return 0;
static int shutUpCounter = 0;
if (shutUpCounter < 5) { // Stop spamming about VRAM reads after the first 5
shutUpCounter++;
Helpers::warn("VRAM read!\n");
}
// TODO: Properly handle framebuffer readbacks and the like
return *(u32*)&vram[vaddr - VirtualAddrs::VramStart];
}
Helpers::panic("Unimplemented 32-bit read, addr: %08X", vaddr);
@ -464,3 +502,15 @@ u64 Memory::timeSince3DSEpoch() {
milliseconds ms = duration_cast<milliseconds>(seconds(rawTime + timezoneDifference + offset));
return ms.count();
}
Regions Memory::getConsoleRegion() {
// TODO: Let the user force the console region as they want
// For now we pick one based on the ROM header
return region;
}
void Memory::copySharedFont(u8* pointer) {
auto fonts = cmrc::ConsoleFonts::get_filesystem();
auto font = fonts.open("CitraSharedFontUSRelocated.bin");
std::memcpy(pointer, font.begin(), font.size());
}

View file

@ -10,6 +10,15 @@ void GLStateManager::resetBlend() {
OpenGL::setLogicOp(GL_COPY);
}
void GLStateManager::resetClearing() {
clearRed = 0.f;
clearBlue = 0.f;
clearGreen = 0.f;
clearAlpha = 1.f;
OpenGL::setClearColor(clearRed, clearBlue, clearGreen, clearAlpha);
}
void GLStateManager::resetClipping() {
// Disable all (supported) clip planes
enabledClipPlanes = 0;
@ -64,6 +73,7 @@ void GLStateManager::resetProgram() {
void GLStateManager::reset() {
resetBlend();
resetClearing();
resetClipping();
resetColourMask();
resetDepth();

View file

@ -7,6 +7,7 @@
#include "PICA/float_types.hpp"
#include "PICA/gpu.hpp"
#include "PICA/regs.hpp"
#include "math_util.hpp"
CMRC_DECLARE(RendererGL);
@ -117,6 +118,7 @@ void RendererGL::initGraphicsContext(SDL_Window* window) {
dummyVBO.create();
dummyVAO.create();
gl.disableScissor();
// Create texture and framebuffer for the 3DS screen
const u32 screenTextureWidth = 400; // Top screen is 400 pixels wide, bottom is 320
@ -125,6 +127,24 @@ void RendererGL::initGraphicsContext(SDL_Window* window) {
glGenTextures(1, &lightLUTTextureArray);
auto prevTexture = OpenGL::getTex2D();
// Create a plain black texture for when a game reads an invalid texture. It is common for games to configure the PICA to read texture info from NULL.
// Some games that do this are Pokemon X, Cars 2, Tomodachi Life, and more. We bind the texture to an FBO, clear it, and free the FBO
blankTexture.create(8, 8, GL_RGBA8);
blankTexture.bind();
blankTexture.setMinFilter(OpenGL::Linear);
blankTexture.setMagFilter(OpenGL::Linear);
OpenGL::Framebuffer dummyFBO;
dummyFBO.createWithDrawTexture(blankTexture); // Create FBO and bind our texture to it
dummyFBO.bind(OpenGL::DrawFramebuffer);
// Clear the texture and then delete FBO
OpenGL::setViewport(8, 8);
gl.setClearColour(0.0, 0.0, 0.0, 1.0);
OpenGL::clearColor();
dummyFBO.free();
screenTexture.create(screenTextureWidth, screenTextureHeight, GL_RGBA8);
screenTexture.bind();
screenTexture.setMinFilter(OpenGL::Linear);
@ -134,13 +154,14 @@ void RendererGL::initGraphicsContext(SDL_Window* window) {
screenFramebuffer.createWithDrawTexture(screenTexture);
screenFramebuffer.bind(OpenGL::DrawAndReadFramebuffer);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) Helpers::panic("Incomplete framebuffer");
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
Helpers::panic("Incomplete framebuffer");
}
// TODO: This should not clear the framebuffer contents. It should load them from VRAM.
GLint oldViewport[4];
glGetIntegerv(GL_VIEWPORT, oldViewport);
OpenGL::setViewport(screenTextureWidth, screenTextureHeight);
OpenGL::setClearColor(0.0, 0.0, 0.0, 1.0);
OpenGL::clearColor();
OpenGL::setViewport(oldViewport[0], oldViewport[1], oldViewport[2], oldViewport[3]);
@ -318,9 +339,17 @@ void RendererGL::bindTexturesToSlots() {
u32 format = regs[ioBase + (i == 0 ? 13 : 5)] & 0xF;
glActiveTexture(GL_TEXTURE0 + i);
Texture targetTex(addr, static_cast<PICA::TextureFmt>(format), width, height, config);
OpenGL::Texture tex = getTexture(targetTex);
tex.bind();
if (addr != 0) [[likely]] {
Texture targetTex(addr, static_cast<PICA::TextureFmt>(format), width, height, config);
OpenGL::Texture tex = getTexture(targetTex);
tex.bind();
} else {
// Mapping a texture from NULL. PICA seems to read the last sampled colour, but for now we will display a black texture instead since it is far easier.
// Games that do this don't really care what it does, they just expect the PICA to not crash, since it doesn't have a PU/MMU and can do all sorts of
// Weird invalid memory accesses without crashing
blankTexture.bind();
}
}
glActiveTexture(GL_TEXTURE0 + 3);
@ -368,8 +397,8 @@ void RendererGL::drawVertices(PICA::PrimType primType, std::span<const Vertex> v
}
setupBlending();
OpenGL::Framebuffer poop = getColourFBO();
poop.bind(OpenGL::DrawAndReadFramebuffer);
auto poop = getColourBuffer(colourBufferLoc, colourBufferFormat, fbSize[0], fbSize[1]);
poop->fbo.bind(OpenGL::DrawAndReadFramebuffer);
const u32 depthControl = regs[PICA::InternalRegs::DepthAndColorMask];
const bool depthWrite = regs[PICA::InternalRegs::DepthBufferWrite];
@ -412,10 +441,12 @@ void RendererGL::drawVertices(PICA::PrimType primType, std::span<const Vertex> v
updateLightingLUT();
}
// TODO: Actually use this
GLsizei viewportWidth = GLsizei(f24::fromRaw(regs[PICA::InternalRegs::ViewportWidth] & 0xffffff).toFloat32() * 2.0f);
GLsizei viewportHeight = GLsizei(f24::fromRaw(regs[PICA::InternalRegs::ViewportHeight] & 0xffffff).toFloat32() * 2.0f);
OpenGL::setViewport(viewportWidth, viewportHeight);
const GLsizei viewportX = regs[PICA::InternalRegs::ViewportXY] & 0x3ff;
const GLsizei viewportY = (regs[PICA::InternalRegs::ViewportXY] >> 16) & 0x3ff;
const GLsizei viewportWidth = GLsizei(f24::fromRaw(regs[PICA::InternalRegs::ViewportWidth] & 0xffffff).toFloat32() * 2.0f);
const GLsizei viewportHeight = GLsizei(f24::fromRaw(regs[PICA::InternalRegs::ViewportHeight] & 0xffffff).toFloat32() * 2.0f);
const auto rect = poop->getSubRect(colourBufferLoc, fbSize[0], fbSize[1]);
OpenGL::setViewport(rect.left + viewportX, rect.bottom + viewportY, viewportWidth, viewportHeight);
const u32 stencilConfig = regs[PICA::InternalRegs::StencilTest];
const bool stencilEnable = getBit<0>(stencilConfig);
@ -450,6 +481,42 @@ void RendererGL::drawVertices(PICA::PrimType primType, std::span<const Vertex> v
void RendererGL::display() {
gl.disableScissor();
gl.disableBlend();
gl.disableDepth();
gl.disableScissor();
// This will work fine whether or not logic ops are enabled. We set logic op to copy instead of disabling to avoid state changes
gl.setLogicOp(GL_COPY);
gl.setColourMask(true, true, true, true);
gl.useProgram(displayProgram);
gl.bindVAO(dummyVAO);
gl.disableClipPlane(0);
gl.disableClipPlane(1);
screenFramebuffer.bind(OpenGL::DrawFramebuffer);
gl.setClearColour(0.f, 0.f, 0.f, 1.f);
OpenGL::clearColor();
using namespace PICA::ExternalRegs;
const u32 topActiveFb = externalRegs[Framebuffer0Select] & 1;
const u32 topScreenAddr = externalRegs[topActiveFb == 0 ? Framebuffer0AFirstAddr : Framebuffer0ASecondAddr];
auto topScreen = colourBufferCache.findFromAddress(topScreenAddr);
if (topScreen) {
topScreen->get().texture.bind();
OpenGL::setViewport(0, 240, 400, 240); // Top screen viewport
OpenGL::draw(OpenGL::TriangleStrip, 4); // Actually draw our 3DS screen
}
const u32 bottomActiveFb = externalRegs[Framebuffer1Select] & 1;
const u32 bottomScreenAddr = externalRegs[bottomActiveFb == 0 ? Framebuffer1AFirstAddr : Framebuffer1ASecondAddr];
auto bottomScreen = colourBufferCache.findFromAddress(bottomScreenAddr);
if (bottomScreen) {
bottomScreen->get().texture.bind();
OpenGL::setViewport(40, 0, 320, 240);
OpenGL::draw(OpenGL::TriangleStrip, 4);
}
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
screenFramebuffer.bind(OpenGL::ReadFramebuffer);
@ -467,8 +534,9 @@ void RendererGL::clearBuffer(u32 startAddress, u32 endAddress, u32 value, u32 co
const float b = getBits<8, 8>(value) / 255.0f;
const float a = (value & 0xff) / 255.0f;
color->get().fbo.bind(OpenGL::DrawFramebuffer);
gl.setColourMask(true, true, true, true);
OpenGL::setClearColor(r, g, b, a);
gl.setClearColour(r, g, b, a);
OpenGL::clearColor();
return;
}
@ -505,7 +573,7 @@ void RendererGL::clearBuffer(u32 startAddress, u32 endAddress, u32 value, u32 co
OpenGL::Framebuffer RendererGL::getColourFBO() {
// We construct a colour buffer object and see if our cache has any matching colour buffers in it
// If not, we allocate a texture & FBO for our framebuffer and store it in the cache
// If not, we allocate a texture & FBO for our framebuffer and store it in the cache
ColourBuffer sampleBuffer(colourBufferLoc, colourBufferFormat, fbSize[0], fbSize[1]);
auto buffer = colourBufferCache.find(sampleBuffer);
@ -550,42 +618,152 @@ OpenGL::Texture RendererGL::getTexture(Texture& tex) {
}
}
// NOTE: The GPU format has RGB5551 and RGB655 swapped compared to internal regs format
PICA::ColorFmt ToColorFmt(u32 format) {
switch (format) {
case 2: return PICA::ColorFmt::RGB565;
case 3: return PICA::ColorFmt::RGBA5551;
default: return static_cast<PICA::ColorFmt>(format);
}
}
void RendererGL::displayTransfer(u32 inputAddr, u32 outputAddr, u32 inputSize, u32 outputSize, u32 flags) {
const u32 inputWidth = inputSize & 0xffff;
const u32 inputGap = inputSize >> 16;
const u32 inputHeight = inputSize >> 16;
const auto inputFormat = ToColorFmt(Helpers::getBits<8, 3>(flags));
const auto outputFormat = ToColorFmt(Helpers::getBits<12, 3>(flags));
const bool verticalFlip = flags & 1;
const PICA::Scaling scaling = static_cast<PICA::Scaling>(Helpers::getBits<24, 2>(flags));
const u32 outputWidth = outputSize & 0xffff;
const u32 outputGap = outputSize >> 16;
u32 outputWidth = outputSize & 0xffff;
u32 outputHeight = outputSize >> 16;
auto framebuffer = colourBufferCache.findFromAddress(inputAddr);
// If there's a framebuffer at this address, use it. Otherwise go back to our old hack and display framebuffer 0
// Displays are hard I really don't want to try implementing them because getting a fast solution is terrible
OpenGL::Texture& tex = framebuffer.has_value() ? framebuffer.value().get().texture : colourBufferCache[0].texture;
OpenGL::DebugScope scope("DisplayTransfer inputAddr 0x%08X outputAddr 0x%08X inputWidth %d outputWidth %d inputHeight %d outputHeight %d",
inputAddr, outputAddr, inputWidth, outputWidth, inputHeight, outputHeight);
tex.bind();
screenFramebuffer.bind(OpenGL::DrawFramebuffer);
auto srcFramebuffer = getColourBuffer(inputAddr, inputFormat, inputWidth, outputHeight);
Math::Rect<u32> srcRect = srcFramebuffer->getSubRect(inputAddr, outputWidth, outputHeight);
gl.disableBlend();
gl.disableLogicOp();
gl.disableDepth();
gl.disableScissor();
gl.disableStencil();
gl.setColourMask(true, true, true, true);
gl.useProgram(displayProgram);
gl.bindVAO(dummyVAO);
gl.disableClipPlane(0);
gl.disableClipPlane(1);
// Hack: Detect whether we are writing to the top or bottom screen by checking output gap and drawing to the proper part of the output texture
// We consider output gap == 320 to mean bottom, and anything else to mean top
if (outputGap == 320) {
OpenGL::setViewport(40, 0, 320, 240); // Bottom screen viewport
} else {
OpenGL::setViewport(0, 240, 400, 240); // Top screen viewport
if (verticalFlip) {
std::swap(srcRect.bottom, srcRect.top);
}
OpenGL::draw(OpenGL::TriangleStrip, 4); // Actually draw our 3DS screen
// Apply scaling for the destination rectangle.
if (scaling == PICA::Scaling::X || scaling == PICA::Scaling::XY) {
outputWidth >>= 1;
}
if (scaling == PICA::Scaling::XY) {
outputHeight >>= 1;
}
auto destFramebuffer = getColourBuffer(outputAddr, outputFormat, outputWidth, outputHeight);
Math::Rect<u32> destRect = destFramebuffer->getSubRect(outputAddr, outputWidth, outputHeight);
if (inputWidth != outputWidth) {
// Helpers::warn("Strided display transfer is not handled correctly!\n");
}
// Blit the framebuffers
srcFramebuffer->fbo.bind(OpenGL::ReadFramebuffer);
destFramebuffer->fbo.bind(OpenGL::DrawFramebuffer);
gl.disableScissor();
glBlitFramebuffer(
srcRect.left, srcRect.bottom, srcRect.right, srcRect.top, destRect.left, destRect.bottom, destRect.right, destRect.top, GL_COLOR_BUFFER_BIT,
GL_LINEAR
);
}
void RendererGL::textureCopy(u32 inputAddr, u32 outputAddr, u32 totalBytes, u32 inputSize, u32 outputSize, u32 flags) {
// Texture copy size is aligned to 16 byte units
const u32 copySize = totalBytes & ~0xf;
if (copySize == 0) {
printf("TextureCopy total bytes less than 16!\n");
return;
}
// The width and gap are provided in 16-byte units.
const u32 inputWidth = (inputSize & 0xffff) << 4;
const u32 inputGap = (inputSize >> 16) << 4;
const u32 outputWidth = (outputSize & 0xffff) << 4;
const u32 outputGap = (outputSize >> 16) << 4;
OpenGL::DebugScope scope("TextureCopy inputAddr 0x%08X outputAddr 0x%08X totalBytes %d inputWidth %d inputGap %d outputWidth %d outputGap %d",
inputAddr, outputAddr, totalBytes, inputWidth, inputGap, outputWidth, outputGap);
if (inputGap != 0 || outputGap != 0) {
// Helpers::warn("Strided texture copy\n");
}
if (inputWidth != outputWidth) {
Helpers::warn("Input width does not match output width, cannot accelerate texture copy!");
return;
}
// Texture copy is a raw data copy in PICA, which means no format or tiling information is provided to the engine.
// Depending if the target surface is linear or tiled, games set inputWidth to either the width of the texture or
// the width multiplied by eight (because tiles are stored linearly in memory).
// To properly accelerate this we must examine each surface individually. For now we assume the most common case
// of tiled surface with RGBA8 format. If our assumption does not hold true, we abort the texture copy as inserting
// that surface is not correct.
// We assume the source surface is tiled and RGBA8. inputWidth is in bytes so divide it
// by eight * sizePerPixel(RGBA8) to convert it to a useable width.
const u32 bpp = sizePerPixel(PICA::ColorFmt::RGBA8);
const u32 copyStride = (inputWidth + inputGap) / (8 * bpp);
const u32 copyWidth = inputWidth / (8 * bpp);
// inputHeight/outputHeight are typically set to zero so they cannot be used to get the height of the copy region
// in contrast to display transfer. Compute height manually by dividing the copy size with the copy width. The result
// is the number of vertical tiles so multiply that by eight to get the actual copy height.
const u32 copyHeight = (copySize / inputWidth) * 8;
// Find the source surface.
auto srcFramebuffer = getColourBuffer(inputAddr, PICA::ColorFmt::RGBA8, copyStride, copyHeight, false);
if (!srcFramebuffer) {
static int shutUpCounter = 0; // Don't want to spam the console too much, so shut up after 5 times
if (shutUpCounter < 5) {
shutUpCounter++;
printf("RendererGL::TextureCopy failed to locate src framebuffer!\n");
}
return;
}
Math::Rect<u32> srcRect = srcFramebuffer->getSubRect(inputAddr, copyWidth, copyHeight);
// Assume the destination surface has the same format. Unless the surfaces have the same block width,
// texture copy does not make sense.
auto destFramebuffer = getColourBuffer(outputAddr, srcFramebuffer->format, copyWidth, copyHeight);
Math::Rect<u32> destRect = destFramebuffer->getSubRect(outputAddr, copyWidth, copyHeight);
// Blit the framebuffers
srcFramebuffer->fbo.bind(OpenGL::ReadFramebuffer);
destFramebuffer->fbo.bind(OpenGL::DrawFramebuffer);
gl.disableScissor();
glBlitFramebuffer(
srcRect.left, srcRect.bottom, srcRect.right, srcRect.top, destRect.left, destRect.bottom, destRect.right, destRect.top, GL_COLOR_BUFFER_BIT,
GL_LINEAR
);
}
std::optional<ColourBuffer> RendererGL::getColourBuffer(u32 addr, PICA::ColorFmt format, u32 width, u32 height, bool createIfnotFound) {
// Try to find an already existing buffer that contains the provided address
// This is a more relaxed check compared to getColourFBO as display transfer/texcopy may refer to
// subrect of a surface and in case of texcopy we don't know the format of the surface.
auto buffer = colourBufferCache.findFromAddress(addr);
if (buffer.has_value()) {
return buffer.value().get();
}
if (!createIfnotFound) {
return std::nullopt;
}
// Otherwise create and cache a new buffer.
ColourBuffer sampleBuffer(addr, format, width, height);
return colourBufferCache.add(sampleBuffer);
}
void RendererGL::screenshot(const std::string& name) {

View file

@ -9,6 +9,11 @@ void Texture::allocate() {
texture.create(size.u(), size.v(), GL_RGBA8);
texture.bind();
#ifdef GPU_DEBUG_INFO
const auto name = Helpers::format("Surface %dx%d %s from 0x%08X", size.x(), size.y(), PICA::textureFormatToString(format), location);
OpenGL::setObjectLabel(GL_TEXTURE, texture.handle(), name.c_str());
#endif
setNewConfig(config);
}

View file

@ -1,6 +1,7 @@
#include "renderer_null/renderer_null.hpp"
RendererNull::RendererNull(GPU& gpu, const std::array<u32, regNum>& internalRegs) : Renderer(gpu, internalRegs) {}
RendererNull::RendererNull(GPU& gpu, const std::array<u32, regNum>& internalRegs, const std::array<u32, extRegNum>& externalRegs)
: Renderer(gpu, internalRegs, externalRegs) {}
RendererNull::~RendererNull() {}
void RendererNull::reset() {}
@ -8,5 +9,6 @@ void RendererNull::display() {}
void RendererNull::initGraphicsContext(SDL_Window* window) {}
void RendererNull::clearBuffer(u32 startAddress, u32 endAddress, u32 value, u32 control) {}
void RendererNull::displayTransfer(u32 inputAddr, u32 outputAddr, u32 inputSize, u32 outputSize, u32 flags) {}
void RendererNull::textureCopy(u32 inputAddr, u32 outputAddr, u32 totalBytes, u32 inputSize, u32 outputSize, u32 flags) {}
void RendererNull::drawVertices(PICA::PrimType primType, std::span<const PICA::Vertex> vertices) {}
void RendererNull::screenshot(const std::string& name) {}
void RendererNull::screenshot(const std::string& name) {}

View file

@ -1,6 +1,7 @@
#include "renderer_sw/renderer_sw.hpp"
RendererSw::RendererSw(GPU& gpu, const std::array<u32, regNum>& internalRegs) : Renderer(gpu, internalRegs) {}
RendererSw::RendererSw(GPU& gpu, const std::array<u32, regNum>& internalRegs, const std::array<u32, extRegNum>& externalRegs)
: Renderer(gpu, internalRegs, externalRegs) {}
RendererSw::~RendererSw() {}
void RendererSw::reset() { printf("RendererSW: Unimplemented reset call\n"); }
@ -13,8 +14,12 @@ void RendererSw::displayTransfer(u32 inputAddr, u32 outputAddr, u32 inputSize, u
printf("RendererSW: Unimplemented displayTransfer call\n");
}
void RendererSw::textureCopy(u32 inputAddr, u32 outputAddr, u32 totalBytes, u32 inputSize, u32 outputSize, u32 flags) {
printf("RendererSW: Unimplemented textureCopy call\n");
}
void RendererSw::drawVertices(PICA::PrimType primType, std::span<const PICA::Vertex> vertices) {
printf("RendererSW: Unimplemented drawVertices call\n");
}
void RendererSw::screenshot(const std::string& name) { printf("RendererSW: Unimplemented screenshot call\n"); }
void RendererSw::screenshot(const std::string& name) { printf("RendererSW: Unimplemented screenshot call\n"); }

View file

@ -200,7 +200,8 @@ vk::Result RendererVK::recreateSwapchain(vk::SurfaceKHR surface, vk::Extent2D sw
return vk::Result::eSuccess;
}
RendererVK::RendererVK(GPU& gpu, const std::array<u32, regNum>& internalRegs) : Renderer(gpu, internalRegs) {}
RendererVK::RendererVK(GPU& gpu, const std::array<u32, regNum>& internalRegs, const std::array<u32, extRegNum>& externalRegs)
: Renderer(gpu, internalRegs, externalRegs) {}
RendererVK::~RendererVK() {}
@ -541,6 +542,8 @@ void RendererVK::clearBuffer(u32 startAddress, u32 endAddress, u32 value, u32 co
void RendererVK::displayTransfer(u32 inputAddr, u32 outputAddr, u32 inputSize, u32 outputSize, u32 flags) {}
void RendererVK::textureCopy(u32 inputAddr, u32 outputAddr, u32 totalBytes, u32 inputSize, u32 outputSize, u32 flags) {}
void RendererVK::drawVertices(PICA::PrimType primType, std::span<const PICA::Vertex> vertices) {}
void RendererVK::screenshot(const std::string& name) {}
void RendererVK::screenshot(const std::string& name) {}

View file

@ -3,7 +3,11 @@
namespace ACCommands {
enum : u32 {
CreateDefaultConfig = 0x00010000,
CancelConnectAsync = 0x00070002,
CloseAsync = 0x00080004,
GetLastErrorCode = 0x000A0000,
RegisterDisconnectEvent = 0x00300004,
SetClientVersion = 0x00400042,
};
}
@ -13,12 +17,40 @@ void ACService::reset() {}
void ACService::handleSyncRequest(u32 messagePointer) {
const u32 command = mem.read32(messagePointer);
switch (command) {
case ACCommands::CancelConnectAsync: cancelConnectAsync(messagePointer); break;
case ACCommands::CloseAsync: closeAsync(messagePointer); break;
case ACCommands::CreateDefaultConfig: createDefaultConfig(messagePointer); break;
case ACCommands::GetLastErrorCode: getLastErrorCode(messagePointer); break;
case ACCommands::RegisterDisconnectEvent: registerDisconnectEvent(messagePointer); break;
case ACCommands::SetClientVersion: setClientVersion(messagePointer); break;
default: Helpers::panic("AC service requested. Command: %08X\n", command);
}
}
void ACService::cancelConnectAsync(u32 messagePointer) {
log("AC::CancelCommandAsync (stubbed)\n");
// TODO: Verify if this response header is correct on hardware
mem.write32(messagePointer, IPC::responseHeader(0x7, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void ACService::closeAsync(u32 messagePointer) {
log("AC::CloseAsync (stubbed)\n");
// TODO: Verify if this response header is correct on hardware
mem.write32(messagePointer, IPC::responseHeader(0x8, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void ACService::createDefaultConfig(u32 messagePointer) {
log("AC::CreateDefaultConfig (stubbed)\n");
mem.write32(messagePointer, IPC::responseHeader(0x1, 1, 2));
mem.write32(messagePointer + 4, Result::Success);
// TODO: Verify response buffer on hardware
}
void ACService::getLastErrorCode(u32 messagePointer) {
log("AC::GetLastErrorCode (stubbed)\n");
@ -33,4 +65,15 @@ void ACService::setClientVersion(u32 messagePointer) {
mem.write32(messagePointer, IPC::responseHeader(0x40, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void ACService::registerDisconnectEvent(u32 messagePointer) {
log("AC::RegisterDisconnectEvent (stubbed)\n");
const u32 pidHeader = mem.read32(messagePointer + 4);
const u32 copyHandleHeader = mem.read32(messagePointer + 12);
// Event signaled when disconnecting from AC
const Handle eventHandle = mem.read32(messagePointer + 16);
mem.write32(messagePointer, IPC::responseHeader(0x30, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}

View file

@ -4,7 +4,8 @@
namespace AMCommands {
enum : u32 {
GetDLCTitleInfo = 0x10050084,
ListTitleInfo = 0x10070102
ListTitleInfo = 0x10070102,
GetPatchTitleInfo = 0x100D0084,
};
}
@ -13,6 +14,7 @@ void AMService::reset() {}
void AMService::handleSyncRequest(u32 messagePointer) {
const u32 command = mem.read32(messagePointer);
switch (command) {
case AMCommands::GetPatchTitleInfo: getPatchTitleInfo(messagePointer); break;
case AMCommands::GetDLCTitleInfo: getDLCTitleInfo(messagePointer); break;
case AMCommands::ListTitleInfo: listTitleInfo(messagePointer); break;
default: Helpers::panic("AM service requested. Command: %08X\n", command);
@ -42,7 +44,16 @@ void AMService::listTitleInfo(u32 messagePointer) {
void AMService::getDLCTitleInfo(u32 messagePointer) {
log("AM::GetDLCTitleInfo (stubbed to fail)\n");
Helpers::warn("Unimplemented AM::GetDLCTitleInfo. Will need to be implemented to support DLC\n");
mem.write32(messagePointer, IPC::responseHeader(0x1005, 1, 4));
mem.write32(messagePointer + 4, -1);
mem.write32(messagePointer + 4, Result::FailurePlaceholder);
}
void AMService::getPatchTitleInfo(u32 messagePointer) {
log("AM::GetPatchTitleInfo (stubbed to fail)\n");
Helpers::warn("Unimplemented AM::GetDLCTitleInfo. Will need to be implemented to support updates\n");
mem.write32(messagePointer, IPC::responseHeader(0x100D, 1, 4));
mem.write32(messagePointer + 4, Result::FailurePlaceholder);
}

View file

@ -2,6 +2,8 @@
#include "ipc.hpp"
#include "kernel.hpp"
#include <vector>
namespace APTCommands {
enum : u32 {
GetLockHandle = 0x00010040,
@ -114,10 +116,24 @@ void APTService::appletUtility(u32 messagePointer) {
u32 outputSize = mem.read32(messagePointer + 12);
u32 inputPointer = mem.read32(messagePointer + 20);
log("APT::AppletUtility(utility = %d, input size = %x, output size = %x, inputPointer = %08X) (Stubbed)\n", utility, inputSize,
outputSize, inputPointer);
log("APT::AppletUtility(utility = %d, input size = %x, output size = %x, inputPointer = %08X) (Stubbed)\n", utility, inputSize, outputSize,
inputPointer);
std::vector<u8> out(outputSize);
const u32 outputBuffer = mem.read32(messagePointer + 0x104);
if (outputSize >= 1 && utility == 6) {
// TryLockTransition expects a bool indicating success in the output buffer. Set it to true to avoid games panicking (Thanks to Citra)
out[0] = true;
}
mem.write32(messagePointer, IPC::responseHeader(0x4B, 2, 2));
mem.write32(messagePointer + 4, Result::Success);
mem.write32(messagePointer + 8, Result::Success);
for (u32 i = 0; i < outputSize; i++) {
mem.write8(outputBuffer + i, out[i]);
}
}
void APTService::getAppletInfo(u32 messagePointer) {

View file

@ -6,15 +6,24 @@ namespace BOSSCommands {
InitializeSession = 0x00010082,
UnregisterStorage = 0x00030000,
GetTaskStorageInfo = 0x00040000,
RegisterNewArrivalEvent = 0x00080002,
GetOptoutFlag = 0x000A0000,
RegisterTask = 0x000B00C2,
UnregisterTask = 0x000C0082,
GetTaskIdList = 0x000E0000,
GetNsDataIdList = 0x00100102,
GetNsDataIdList1 = 0x00110102,
SendProperty = 0x00140082,
ReceiveProperty = 0x00160082,
GetTaskServiceStatus = 0x001B0042,
StartTask = 0x001C0042,
CancelTask = 0x001E0042,
GetTaskState = 0x00200082,
GetTaskStatus = 0x002300C2,
GetTaskInfo = 0x00250082,
GetErrorCode = 0x002E0040,
RegisterStorageEntry = 0x002F0140,
GetStorageEntryInfo = 0x00300000
GetStorageEntryInfo = 0x00300000,
};
}
@ -26,15 +35,25 @@ void BOSSService::handleSyncRequest(u32 messagePointer) {
const u32 command = mem.read32(messagePointer);
switch (command) {
case BOSSCommands::CancelTask: cancelTask(messagePointer); break;
case BOSSCommands::GetNsDataIdList: getNsDataIdList(messagePointer); break;
case BOSSCommands::GetErrorCode: getErrorCode(messagePointer); break;
case BOSSCommands::GetNsDataIdList:
case BOSSCommands::GetNsDataIdList1:
getNsDataIdList(messagePointer, command); break;
case BOSSCommands::GetOptoutFlag: getOptoutFlag(messagePointer); break;
case BOSSCommands::GetStorageEntryInfo: getStorageEntryInfo(messagePointer); break;
case BOSSCommands::GetTaskIdList: getTaskIdList(messagePointer); break;
case BOSSCommands::GetTaskInfo: getTaskInfo(messagePointer); break;
case BOSSCommands::GetTaskServiceStatus: getTaskServiceStatus(messagePointer); break;
case BOSSCommands::GetTaskState: getTaskState(messagePointer); break;
case BOSSCommands::GetTaskStatus: getTaskStatus(messagePointer); break;
case BOSSCommands::GetTaskStorageInfo: getTaskStorageInfo(messagePointer); break;
case BOSSCommands::InitializeSession: initializeSession(messagePointer); break;
case BOSSCommands::ReceiveProperty: receiveProperty(messagePointer); break;
case BOSSCommands::RegisterNewArrivalEvent: registerNewArrivalEvent(messagePointer); break;
case BOSSCommands::RegisterStorageEntry: registerStorageEntry(messagePointer); break;
case BOSSCommands::RegisterTask: registerTask(messagePointer); break;
case BOSSCommands::SendProperty: sendProperty(messagePointer); break;
case BOSSCommands::StartTask: startTask(messagePointer); break;
case BOSSCommands::UnregisterStorage: unregisterStorage(messagePointer); break;
case BOSSCommands::UnregisterTask: unregisterTask(messagePointer); break;
default: Helpers::panic("BOSS service requested. Command: %08X\n", command);
@ -48,17 +67,51 @@ void BOSSService::initializeSession(u32 messagePointer) {
}
void BOSSService::getOptoutFlag(u32 messagePointer) {
log("BOSS::getOptoutFlag\n");
log("BOSS::GetOptoutFlag\n");
mem.write32(messagePointer, IPC::responseHeader(0xA, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write8(messagePointer + 8, optoutFlag);
}
void BOSSService::getTaskState(u32 messagePointer) {
const u32 taskIDBufferSize = mem.read32(messagePointer + 4);
const u32 taskIDDataPointer = mem.read32(messagePointer + 16);
log("BOSS::GetTaskStatus (task buffer size: %08X, task data pointer: %08X) (stubbed)\n", taskIDBufferSize, taskIDDataPointer);
mem.write32(messagePointer, IPC::responseHeader(0x20, 2, 2));
mem.write32(messagePointer + 4, Result::Success);
mem.write8(messagePointer + 8, 0); // TaskStatus: Report the task finished successfully
mem.write32(messagePointer + 12, 0); // Current state value for task PropertyID 0x4
mem.write8(messagePointer + 16, 0); // TODO: Figure out what this should be
}
void BOSSService::getTaskStatus(u32 messagePointer) {
// TODO: 3DBrew does not mention what the parameters are, or what the return values are.
log("BOSS::GetTaskStatus (Stubbed)\n");
// Response values stubbed based on Citra
mem.write32(messagePointer, IPC::responseHeader(0x23, 2, 2));
mem.write32(messagePointer + 4, Result::Success);
mem.write8(messagePointer + 8, 0);
// TODO: Citra pushes a buffer here?
}
void BOSSService::getTaskServiceStatus(u32 messagePointer) {
// TODO: 3DBrew does not mention what the parameters are, or what the return values are... again
log("BOSS::GetTaskServiceStatus (Stubbed)\n");
// Response values stubbed based on Citra
mem.write32(messagePointer, IPC::responseHeader(0x1B, 2, 2));
mem.write32(messagePointer + 4, Result::Success);
mem.write8(messagePointer + 8, 0);
// TODO: Citra pushes a buffer here too?
}
void BOSSService::getTaskStorageInfo(u32 messagePointer) {
log("BOSS::GetTaskStorageInfo (stubbed)\n");
mem.write32(messagePointer, IPC::responseHeader(0x4, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write32(messagePointer + 8, 0); // Seems to be unknown what this is?
mem.write32(messagePointer + 8, 0);
}
void BOSSService::getTaskIdList(u32 messagePointer) {
@ -76,6 +129,13 @@ void BOSSService::getTaskInfo(u32 messagePointer) {
mem.write32(messagePointer + 4, Result::Success);
}
void BOSSService::getErrorCode(u32 messagePointer) {
log("BOSS::GetErrorCode (stubbed)\n");
mem.write32(messagePointer, IPC::responseHeader(0x2E, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write32(messagePointer + 8, Result::Success); // No error code
}
void BOSSService::getStorageEntryInfo(u32 messagePointer) {
log("BOSS::GetStorageEntryInfo (undocumented)\n");
mem.write32(messagePointer, IPC::responseHeader(0x30, 3, 0));
@ -84,33 +144,76 @@ void BOSSService::getStorageEntryInfo(u32 messagePointer) {
mem.write16(messagePointer + 12, 0); // s16, unknown meaning
}
void BOSSService::sendProperty(u32 messagePointer) {
const u32 id = mem.read32(messagePointer + 4);
const u32 size = mem.read32(messagePointer + 8);
const u32 ptr = mem.read32(messagePointer + 16);
log("BOSS::SendProperty (id = %d, size = %08X, ptr = %08X) (stubbed)\n", id, size, ptr);
mem.write32(messagePointer, IPC::responseHeader(0x14, 1, 2));
mem.write32(messagePointer + 4, Result::Success);
mem.write32(messagePointer + 8, 0); // Read size
// TODO: Should this do anything else?
}
void BOSSService::receiveProperty(u32 messagePointer) {
const u32 id = mem.read32(messagePointer + 4);
const u32 size = mem.read32(messagePointer + 8);
const u32 ptr = mem.read32(messagePointer + 16);
log("BOSS::ReceiveProperty(stubbed) (id = %d, size = %08X, ptr = %08X)\n", id, size, ptr);
log("BOSS::ReceiveProperty (id = %d, size = %08X, ptr = %08X) (stubbed)\n", id, size, ptr);
mem.write32(messagePointer, IPC::responseHeader(0x16, 2, 2));
mem.write32(messagePointer + 4, Result::Success);
mem.write32(messagePointer + 8, 0); // Read size
}
// This seems to accept a KEvent as a parameter and register it for something Spotpass related
// I need to update the 3DBrew page when it's known what it does properly
void BOSSService::registerNewArrivalEvent(u32 messagePointer) {
const Handle eventHandle = mem.read32(messagePointer + 4); // Kernel event handle to register
log("BOSS::RegisterNewArrivalEvent (handle = %X)\n", eventHandle);
mem.write32(messagePointer, IPC::responseHeader(0x8, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void BOSSService::startTask(u32 messagePointer) {
log("BOSS::StartTask (stubbed)\n");
const u32 bufferSize = mem.read32(messagePointer + 4);
const u32 descriptor = mem.read32(messagePointer + 8);
const u32 bufferData = mem.read32(messagePointer + 12);
mem.write32(messagePointer, IPC::responseHeader(0x1C, 1, 2));
mem.write32(messagePointer + 4, Result::Success);
}
void BOSSService::cancelTask(u32 messagePointer) {
log("BOSS::CancelTask (stubbed)\n");
mem.write32(messagePointer, IPC::responseHeader(0x1E, 1, 2));
mem.write32(messagePointer + 4, Result::Success);
}
void BOSSService::registerTask(u32 messagePointer) {
log("BOSS::RegisterTask (stubbed)\n");
const u32 bufferSize = mem.read32(messagePointer + 4);
const u32 dataPointr = mem.read32(messagePointer + 20);
mem.write32(messagePointer, IPC::responseHeader(0x0B, 1, 2));
mem.write32(messagePointer + 4, Result::Success);
}
void BOSSService::unregisterTask(u32 messagePointer) {
log("BOSS::UnregisterTask (stubbed)\n");
mem.write32(messagePointer, IPC::responseHeader(0x0C, 1, 2));
mem.write32(messagePointer + 4, Result::Success);
}
void BOSSService::getNsDataIdList(u32 messagePointer) {
// There's multiple aliases for this command. commandWord is the first word in the IPC buffer with the command word, needed for the response header
void BOSSService::getNsDataIdList(u32 messagePointer, u32 commandWord) {
log("BOSS::GetNsDataIdList (stubbed)\n");
mem.write32(messagePointer, IPC::responseHeader(0x10, 3, 2));
mem.write32(messagePointer, IPC::responseHeader(commandWord >> 16, 3, 2));
mem.write32(messagePointer + 4, Result::Success);
mem.write16(messagePointer + 8, 0); // u16: Actual number of output entries.
mem.write16(messagePointer + 12, 0); // u16: Last word-index copied to output in the internal NsDataId list.

View file

@ -1,19 +1,22 @@
#include "services/cam.hpp"
#include "ipc.hpp"
#include "kernel.hpp"
namespace CAMCommands {
enum : u32 {
GetBufferErrorInterruptEvent = 0x00060040,
DriverInitialize = 0x00390000,
GetMaxLines = 0x000A0080
GetMaxLines = 0x000A0080,
};
}
void CAMService::reset() {}
void CAMService::reset() { bufferErrorInterruptEvents.fill(std::nullopt); }
void CAMService::handleSyncRequest(u32 messagePointer) {
const u32 command = mem.read32(messagePointer);
switch (command) {
case CAMCommands::DriverInitialize: driverInitialize(messagePointer); break;
case CAMCommands::GetBufferErrorInterruptEvent: getBufferErrorInterruptEvent(messagePointer); break;
case CAMCommands::GetMaxLines: getMaxLines(messagePointer); break;
default: Helpers::panic("CAM service requested. Command: %08X\n", command);
}
@ -55,4 +58,24 @@ void CAMService::getMaxLines(u32 messagePointer) {
mem.write32(messagePointer + 4, result);
mem.write16(messagePointer + 8, lines);
}
}
void CAMService::getBufferErrorInterruptEvent(u32 messagePointer) {
const u32 port = mem.read32(messagePointer + 4);
log("CAM::GetBufferErrorInterruptEvent (port = %d)\n", port);
mem.write32(messagePointer, IPC::responseHeader(0x6, 1, 2));
if (port >= portCount) {
Helpers::panic("CAM::GetBufferErrorInterruptEvent: Invalid port");
} else {
auto& event = bufferErrorInterruptEvents[port];
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());
}
}

View file

@ -1,21 +1,22 @@
#include "services/cecd.hpp"
#include "ipc.hpp"
#include "kernel.hpp"
namespace CECDCommands {
enum : u32 {
GetInfoEventHandle = 0x000F0000
GetInfoEventHandle = 0x000F0000,
OpenAndRead = 0x00120104,
};
}
void CECDService::reset() {
infoEvent = std::nullopt;
}
void CECDService::reset() { infoEvent = std::nullopt; }
void CECDService::handleSyncRequest(u32 messagePointer) {
const u32 command = mem.read32(messagePointer);
switch (command) {
case CECDCommands::GetInfoEventHandle: getInfoEventHandle(messagePointer); break;
case CECDCommands::OpenAndRead: openAndRead(messagePointer); break;
default:
Helpers::panicDev("CECD service requested. Command: %08X\n", command);
mem.write32(messagePointer + 4, Result::Success);
@ -34,4 +35,17 @@ void CECDService::getInfoEventHandle(u32 messagePointer) {
mem.write32(messagePointer + 4, Result::Success);
// TODO: Translation descriptor here?
mem.write32(messagePointer + 12, infoEvent.value());
}
void CECDService::openAndRead(u32 messagePointer) {
const u32 bufferSize = mem.read32(messagePointer + 4);
const u32 programID = mem.read32(messagePointer + 8);
const u32 pathType = mem.read32(messagePointer + 12);
const u32 bufferAddress = mem.read32(messagePointer + 32);
log("CECD::OpenAndRead (size = %08X, address = %08X, path type = %d)\n", bufferSize, bufferAddress, pathType);
// TODO: We should implement this properly the time comes
mem.write32(messagePointer, IPC::responseHeader(0x12, 2, 2));
mem.write32(messagePointer + 4, Result::Success);
mem.write32(messagePointer + 8, 0); // Bytes read
}

View file

@ -134,7 +134,7 @@ void CFGService::secureInfoGetRegion(u32 messagePointer) {
mem.write32(messagePointer, IPC::responseHeader(0x2, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write32(messagePointer + 8, static_cast<u32>(Regions::USA)); // TODO: Detect the game region and report it
mem.write32(messagePointer + 8, static_cast<u32>(mem.getConsoleRegion()));
}
void CFGService::genUniqueConsoleHash(u32 messagePointer) {
@ -153,7 +153,16 @@ void CFGService::genUniqueConsoleHash(u32 messagePointer) {
// Used for market restriction-related stuff
void CFGService::getRegionCanadaUSA(u32 messagePointer) {
log("CFG::GetRegionCanadaUSA\n");
const u8 ret = (country == CountryCodes::US || country == CountryCodes::CA) ? 1 : 0;
bool regionUSA = mem.getConsoleRegion() == Regions::USA;
u8 ret;
// First, this function checks that the console region is 1 (USA). If not then it instantly returns 0
// Then it checks whether the country is US or Canda. If yes it returns 1, else it returns 0.
if (!regionUSA) {
ret = 0;
} else {
ret = (country == CountryCodes::US || country == CountryCodes::CA) ? 1 : 0;
}
mem.write32(messagePointer, IPC::responseHeader(0x4, 2, 0));
mem.write32(messagePointer + 4, Result::Success);

Binary file not shown.

View file

@ -1,23 +1,28 @@
#include <string>
#include "services/frd.hpp"
#include "services/region_codes.hpp"
#include <string>
#include "ipc.hpp"
#include "services/region_codes.hpp"
namespace FRDCommands {
enum : u32 {
HasLoggedIn = 0x00010000,
AttachToEventNotification = 0x00200002,
SetNotificationMask = 0x00210040,
SetClientSdkVersion = 0x00320042,
Logout = 0x00040000,
GetMyFriendKey = 0x00050000,
GetMyProfile = 0x00070000,
GetMyPresence = 0x00080000,
GetMyScreenName = 0x00090000,
GetMyMii = 0x000A0000,
GetFriendKeyList = 0x00110080
GetFriendKeyList = 0x00110080,
UpdateGameModeDescription = 0x001D0002,
};
}
void FRDService::reset() {}
void FRDService::reset() { loggedIn = false; }
void FRDService::handleSyncRequest(u32 messagePointer) {
const u32 command = mem.read32(messagePointer);
@ -29,8 +34,11 @@ void FRDService::handleSyncRequest(u32 messagePointer) {
case FRDCommands::GetMyPresence: getMyPresence(messagePointer); break;
case FRDCommands::GetMyProfile: getMyProfile(messagePointer); break;
case FRDCommands::GetMyScreenName: getMyScreenName(messagePointer); break;
case FRDCommands::HasLoggedIn: hasLoggedIn(messagePointer); break;
case FRDCommands::Logout: logout(messagePointer); break;
case FRDCommands::SetClientSdkVersion: setClientSDKVersion(messagePointer); break;
case FRDCommands::SetNotificationMask: setNotificationMask(messagePointer); break;
case FRDCommands::UpdateGameModeDescription: updateGameModeDescription(messagePointer); break;
default: Helpers::panic("FRD service requested. Command: %08X\n", command);
}
}
@ -40,6 +48,14 @@ void FRDService::attachToEventNotification(u32 messagePointer) {
mem.write32(messagePointer + 4, Result::Success);
}
// This is supposed to post stuff on your user profile so uhh can't really emulate it
void FRDService::updateGameModeDescription(u32 messagePointer) {
log("FRD::UpdateGameModeDescription\n");
mem.write32(messagePointer, IPC::responseHeader(0x1D, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void FRDService::getMyFriendKey(u32 messagePointer) {
log("FRD::GetMyFriendKey\n");
@ -134,4 +150,20 @@ void FRDService::getMyMii(u32 messagePointer) {
// TODO: How is the mii data even returned?
mem.write32(messagePointer, IPC::responseHeader(0xA, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void FRDService::hasLoggedIn(u32 messagePointer) {
log("FRD::HasLoggedIn\n");
mem.write32(messagePointer, IPC::responseHeader(0x1, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write8(messagePointer + 8, loggedIn ? 1 : 0);
}
void FRDService::logout(u32 messagePointer) {
log("FRD::Logout\n");
loggedIn = false;
mem.write32(messagePointer, IPC::responseHeader(0x4, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}

View file

@ -27,6 +27,8 @@ namespace FSCommands {
IsSdmcWritable = 0x08180000,
GetFormatInfo = 0x084500C2,
FormatSaveData = 0x084C0242,
CreateExtSaveData = 0x08510242,
DeleteExtSaveData = 0x08520100,
InitializeWithSdkVersion = 0x08610042,
SetPriority = 0x08620040,
GetPriority = 0x08630000
@ -144,9 +146,11 @@ void FSService::handleSyncRequest(u32 messagePointer) {
const u32 command = mem.read32(messagePointer);
switch (command) {
case FSCommands::CreateDirectory: createDirectory(messagePointer); break;
case FSCommands::CreateExtSaveData: createExtSaveData(messagePointer); break;
case FSCommands::CreateFile: createFile(messagePointer); break;
case FSCommands::ControlArchive: controlArchive(messagePointer); break;
case FSCommands::CloseArchive: closeArchive(messagePointer); break;
case FSCommands::DeleteExtSaveData: deleteExtSaveData(messagePointer); break;
case FSCommands::DeleteFile: deleteFile(messagePointer); break;
case FSCommands::FormatSaveData: formatSaveData(messagePointer); break;
case FSCommands::FormatThisUserSaveData: formatThisUserSaveData(messagePointer); break;
@ -455,6 +459,40 @@ void FSService::formatSaveData(u32 messagePointer) {
mem.write32(messagePointer + 4, Result::Success);
}
void FSService::deleteExtSaveData(u32 messagePointer) {
Helpers::warn("Stubbed call to FS::DeleteExtSaveData!");
// First 4 words of parameters are the ExtSaveData info
// https://www.3dbrew.org/wiki/Filesystem_services#ExtSaveDataInfo
const u8 mediaType = mem.read8(messagePointer + 4);
const u64 saveID = mem.read64(messagePointer + 8);
log("FS::DeleteExtSaveData (media type = %d, saveID = %llx) (stubbed)\n", mediaType, saveID);
mem.write32(messagePointer, IPC::responseHeader(0x0852, 1, 0));
// TODO: We can't properly implement this yet until we properly support title/save IDs. We will stub this and insert a warning for now. Required for Planet Robobot
// When we properly implement it, it will just be a recursive directory deletion
mem.write32(messagePointer + 4, Result::Success);
}
void FSService::createExtSaveData(u32 messagePointer) {
Helpers::warn("Stubbed call to FS::CreateExtSaveData!");
// First 4 words of parameters are the ExtSaveData info
// https://www.3dbrew.org/wiki/Filesystem_services#ExtSaveDataInfo
// This creates the ExtSaveData with the specified saveid in the specified media type. It stores the SMDH as "icon" in the root of the created directory.
const u8 mediaType = mem.read8(messagePointer + 4);
const u64 saveID = mem.read64(messagePointer + 8);
const u32 numOfDirectories = mem.read32(messagePointer + 20);
const u32 numOfFiles = mem.read32(messagePointer + 24);
const u64 sizeLimit = mem.read64(messagePointer + 28);
const u32 smdhSize = mem.read32(messagePointer + 36);
const u32 smdhPointer = mem.read32(messagePointer + 44);
log("FS::CreateExtSaveData (stubbed)\n");
mem.write32(messagePointer, IPC::responseHeader(0x0851, 1, 0));
// TODO: Similar to DeleteExtSaveData, we need to refactor how our ExtSaveData stuff works before properly implementing this
mem.write32(messagePointer + 4, Result::Success);
}
void FSService::formatThisUserSaveData(u32 messagePointer) {
log("FS::FormatThisUserSaveData\n");
@ -499,6 +537,12 @@ void FSService::controlArchive(u32 messagePointer) {
case 0: // Commit save data changes. Shouldn't need us to do anything
mem.write32(messagePointer + 4, Result::Success);
break;
case 1: // Retrieves a file's last-modified timestamp. Seen in DDLC, stubbed for the moment
Helpers::warn("FS::ControlArchive: Tried to retrieve a file's last-modified timestamp");
mem.write32(messagePointer + 4, Result::Success);
break;
default:
Helpers::panic("Unimplemented action for ControlArchive (action = %X)\n", action);
break;

View file

@ -1,4 +1,5 @@
#include "services/gsp_gpu.hpp"
#include "PICA/regs.hpp"
#include "ipc.hpp"
#include "kernel.hpp"
@ -10,6 +11,7 @@ namespace ServiceCommands {
RegisterInterruptRelayQueue = 0x00130042,
WriteHwRegs = 0x00010082,
WriteHwRegsWithMask = 0x00020084,
SetBufferSwap = 0x00050200,
FlushDataCache = 0x00080082,
SetLCDForceBlack = 0x000B0040,
TriggerCmdReqQueue = 0x000C0000,
@ -49,12 +51,13 @@ void GPUService::handleSyncRequest(u32 messagePointer) {
case ServiceCommands::RegisterInterruptRelayQueue: registerInterruptRelayQueue(messagePointer); break;
case ServiceCommands::SaveVramSysArea: saveVramSysArea(messagePointer); break;
case ServiceCommands::SetAxiConfigQoSMode: setAxiConfigQoSMode(messagePointer); break;
case ServiceCommands::SetBufferSwap: setBufferSwap(messagePointer); break;
case ServiceCommands::SetInternalPriorities: setInternalPriorities(messagePointer); break;
case ServiceCommands::SetLCDForceBlack: setLCDForceBlack(messagePointer); break;
case ServiceCommands::StoreDataCache: storeDataCache(messagePointer); break;
case ServiceCommands::WriteHwRegs: writeHwRegs(messagePointer); break;
case ServiceCommands::WriteHwRegsWithMask: writeHwRegsWithMask(messagePointer); break;
; default: Helpers::panic("GPU service requested. Command: %08X\n", command);
default: Helpers::panic("GPU service requested. Command: %08X\n", command);
}
}
@ -128,15 +131,12 @@ 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
constexpr u32 FBInfoSize = 0x40;
// TODO: Offset depends on GSP thread being triggered
u8* info = &sharedMem[0x200 + screen * FBInfoSize];
u8& dirtyFlag = info[1];
FramebufferUpdate* update = reinterpret_cast<FramebufferUpdate*>(&sharedMem[0x200 + screen * sizeof(FramebufferUpdate)]);
if (dirtyFlag & 1) {
// TODO: Submit buffer info here
dirtyFlag &= ~1;
if (update->dirtyFlag & 1) {
setBufferSwapImpl(screen, update->framebufferInfo[update->index]);
update->dirtyFlag &= ~1;
}
}
@ -224,7 +224,7 @@ void GPUService::flushDataCache(u32 messagePointer) {
u32 address = mem.read32(messagePointer + 4);
u32 size = mem.read32(messagePointer + 8);
u32 processHandle = handle = mem.read32(messagePointer + 16);
log("GSP::GPU::FlushDataCache(address = %08X, size = %X, process = %X\n", address, size, processHandle);
log("GSP::GPU::FlushDataCache(address = %08X, size = %X, process = %X)\n", address, size, processHandle);
mem.write32(messagePointer, IPC::responseHeader(0x8, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
@ -234,7 +234,7 @@ void GPUService::storeDataCache(u32 messagePointer) {
u32 address = mem.read32(messagePointer + 4);
u32 size = mem.read32(messagePointer + 8);
u32 processHandle = handle = mem.read32(messagePointer + 16);
log("GSP::GPU::StoreDataCache(address = %08X, size = %X, process = %X\n", address, size, processHandle);
log("GSP::GPU::StoreDataCache(address = %08X, size = %X, process = %X)\n", address, size, processHandle);
mem.write32(messagePointer, IPC::responseHeader(0x1F, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
@ -265,6 +265,24 @@ void GPUService::setAxiConfigQoSMode(u32 messagePointer) {
mem.write32(messagePointer + 4, Result::Success);
}
void GPUService::setBufferSwap(u32 messagePointer) {
FramebufferInfo info{};
const u32 screenId = mem.read32(messagePointer + 4); // Selects either PDC0 or PDC1
info.activeFb = mem.read32(messagePointer + 8);
info.leftFramebufferVaddr = mem.read32(messagePointer + 12);
info.rightFramebufferVaddr = mem.read32(messagePointer + 16);
info.stride = mem.read32(messagePointer + 20);
info.format = mem.read32(messagePointer + 24);
info.displayFb = mem.read32(messagePointer + 28); // Selects either framebuffer A or B
log("GSP::GPU::SetBufferSwap\n");
Helpers::panic("Untested GSP::GPU::SetBufferSwap call");
setBufferSwapImpl(screenId, info);
mem.write32(messagePointer, IPC::responseHeader(0x05, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
// Seems to also be completely undocumented
void GPUService::setInternalPriorities(u32 messagePointer) {
log("GSP::GPU::SetInternalPriorities\n");
@ -287,7 +305,7 @@ void GPUService::processCommandBuffer() {
log("Processing %d GPU commands\n", commandsLeft);
while (commandsLeft != 0) {
u32 cmdID = cmd[0] & 0xff;
const u32 cmdID = cmd[0] & 0xff;
switch (cmdID) {
case GXCommands::ProcessCommandList: processCommandList(cmd); break;
case GXCommands::MemoryFill: memoryFill(cmd); break;
@ -379,12 +397,47 @@ void GPUService::flushCacheRegions(u32* cmd) {
log("GSP::GPU::FlushCacheRegions (Stubbed)\n");
}
void GPUService::setBufferSwapImpl(u32 screenId, const FramebufferInfo& info) {
using namespace PICA::ExternalRegs;
static constexpr std::array<u32, 8> fbAddresses = {
Framebuffer0AFirstAddr,
Framebuffer0BFirstAddr,
Framebuffer1AFirstAddr,
Framebuffer1BFirstAddr,
Framebuffer0ASecondAddr,
Framebuffer0BSecondAddr,
Framebuffer1ASecondAddr,
Framebuffer1BSecondAddr,
};
auto& regs = gpu.getExtRegisters();
const u32 fbIndex = info.activeFb * 4 + screenId * 2;
regs[fbAddresses[fbIndex]] = VaddrToPaddr(info.leftFramebufferVaddr);
regs[fbAddresses[fbIndex + 1]] = VaddrToPaddr(info.rightFramebufferVaddr);
static constexpr std::array<u32, 6> configAddresses = {
Framebuffer0Config,
Framebuffer0Select,
Framebuffer0Stride,
Framebuffer1Config,
Framebuffer1Select,
Framebuffer1Stride,
};
const u32 configIndex = screenId * 3;
regs[configAddresses[configIndex]] = info.format;
regs[configAddresses[configIndex + 1]] = info.displayFb;
regs[configAddresses[configIndex + 2]] = info.stride;
}
// Actually send command list (aka display list) to GPU
void GPUService::processCommandList(u32* cmd) {
const u32 address = cmd[1] & ~7; // Buffer address
const u32 size = cmd[2] & ~3; // Buffer size in bytes
const bool updateGas = cmd[3] == 1; // Update gas additive blend results (0 = don't update, 1 = update)
const bool flushBuffer = cmd[7] == 1; // Flush buffer (0 = don't flush, 1 = flush)
[[maybe_unused]] const bool updateGas = cmd[3] == 1; // Update gas additive blend results (0 = don't update, 1 = update)
[[maybe_unused]] const bool flushBuffer = cmd[7] == 1; // Flush buffer (0 = don't flush, 1 = flush)
log("GPU::GSP::processCommandList. Address: %08X, size in bytes: %08X\n", address, size);
gpu.startCommandList(address, size);
@ -394,7 +447,15 @@ void GPUService::processCommandList(u32* cmd) {
// TODO: Emulate the transfer engine & its registers
// Then this can be emulated by just writing the appropriate values there
void GPUService::triggerTextureCopy(u32* cmd) {
Helpers::warn("GSP::GPU::TriggerTextureCopy (unimplemented)\n");
const u32 inputAddr = VaddrToPaddr(cmd[1]);
const u32 outputAddr = VaddrToPaddr(cmd[2]);
const u32 totalBytes = cmd[3];
const u32 inputSize = cmd[4];
const u32 outputSize = cmd[5];
const u32 flags = cmd[6];
log("GSP::GPU::TriggerTextureCopy (Stubbed)\n");
gpu.textureCopy(inputAddr, outputAddr, totalBytes, inputSize, outputSize, flags);
// This uses the transfer engine and thus needs to fire a PPF interrupt.
// NSMB2 relies on this
requestInterrupt(GPUInterrupt::PPF);

View file

@ -11,7 +11,8 @@ namespace HIDCommands {
EnableGyroscopeLow = 0x00130000,
DisableGyroscopeLow = 0x00140000,
GetGyroscopeLowRawToDpsCoefficient = 0x00150000,
GetGyroscopeLowCalibrateParam = 0x00160000
GetGyroscopeLowCalibrateParam = 0x00160000,
GetSoundVolume = 0x00170000,
};
}
@ -46,6 +47,7 @@ void HIDService::handleSyncRequest(u32 messagePointer) {
case HIDCommands::GetGyroscopeLowCalibrateParam: getGyroscopeLowCalibrateParam(messagePointer); break;
case HIDCommands::GetGyroscopeLowRawToDpsCoefficient: getGyroscopeCoefficient(messagePointer); break;
case HIDCommands::GetIPCHandles: getIPCHandles(messagePointer); break;
case HIDCommands::GetSoundVolume: getSoundVolume(messagePointer); break;
default: Helpers::panic("HID service requested. Command: %08X\n", command);
}
}
@ -107,6 +109,18 @@ void HIDService::getGyroscopeCoefficient(u32 messagePointer) {
mem.write32(messagePointer + 8, Helpers::bit_cast<u32, float>(gyroscopeCoeff));
}
// The volume here is in the range [0, 0x3F]
// It is read directly from I2C Device 3 register 0x09
// Since we currently do not have audio, set the volume a bit below max (0x30)
void HIDService::getSoundVolume(u32 messagePointer) {
log("HID::GetSoundVolume\n");
constexpr u8 volume = 0x30;
mem.write32(messagePointer, IPC::responseHeader(0x17, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write8(messagePointer + 8, volume);
}
void HIDService::getIPCHandles(u32 messagePointer) {
log("HID::GetIPCHandles\n");

View file

@ -0,0 +1,27 @@
#include "ipc.hpp"
#include "result/result.hpp"
#include "services/mcu/mcu_hwc.hpp"
namespace MCU::HWCCommands {
enum : u32 {
GetBatteryLevel = 0x00050000,
};
}
void MCU::HWCService::reset() {}
void MCU::HWCService::handleSyncRequest(u32 messagePointer) {
const u32 command = mem.read32(messagePointer);
switch (command) {
case HWCCommands::GetBatteryLevel: getBatteryLevel(messagePointer); break;
default: Helpers::panic("MCU::HWC service requested. Command: %08X\n", command);
}
}
void MCU::HWCService::getBatteryLevel(u32 messagePointer) {
log("MCU::HWC::GetBatteryLevel\n");
mem.write32(messagePointer, IPC::responseHeader(0x5, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write8(messagePointer + 8, config.batteryPercentage);
}

View file

@ -5,6 +5,7 @@ namespace MICCommands {
enum : u32 {
MapSharedMem = 0x00010042,
StartSampling = 0x00030140,
StopSampling = 0x00050000,
SetGain = 0x00080040,
GetGain = 0x00090000,
SetPower = 0x000A0040,
@ -17,6 +18,7 @@ namespace MICCommands {
void MICService::reset() {
micEnabled = false;
shouldClamp = false;
isSampling = false;
gain = 0;
}
@ -30,6 +32,7 @@ void MICService::handleSyncRequest(u32 messagePointer) {
case MICCommands::SetIirFilter: setIirFilter(messagePointer); break;
case MICCommands::SetPower: setPower(messagePointer); break;
case MICCommands::StartSampling: startSampling(messagePointer); break;
case MICCommands::StopSampling: stopSampling(messagePointer); break;
case MICCommands::CaptainToadFunction: theCaptainToadFunction(messagePointer); break;
default: Helpers::panic("MIC service requested. Command: %08X\n", command);
}
@ -88,10 +91,19 @@ void MICService::startSampling(u32 messagePointer) {
encoding, sampleRate, offset, dataSize, loop ? "yes" : "no"
);
isSampling = true;
mem.write32(messagePointer, IPC::responseHeader(0x3, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void MICService::stopSampling(u32 messagePointer) {
log("MIC::StopSampling\n");
isSampling = false;
mem.write32(messagePointer, IPC::responseHeader(0x5, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void MICService::setIirFilter(u32 messagePointer) {
const u32 size = mem.read32(messagePointer + 4);
const u32 pointer = mem.read32(messagePointer + 12);

View file

@ -7,7 +7,8 @@ namespace NDMCommands {
SuspendDaemons = 0x00060040,
ResumeDaemons = 0x00070040,
SuspendScheduler = 0x00080040,
ResumeScheduler = 0x00090000
ResumeScheduler = 0x00090000,
ClearHalfAwakeMacFilter = 0x00170000,
};
}
@ -16,6 +17,7 @@ void NDMService::reset() {}
void NDMService::handleSyncRequest(u32 messagePointer) {
const u32 command = mem.read32(messagePointer);
switch (command) {
case NDMCommands::ClearHalfAwakeMacFilter: clearHalfAwakeMacFilter(messagePointer); break;
case NDMCommands::OverrideDefaultDaemons: overrideDefaultDaemons(messagePointer); break;
case NDMCommands::ResumeDaemons: resumeDaemons(messagePointer); break;
case NDMCommands::ResumeScheduler: resumeScheduler(messagePointer); break;
@ -26,31 +28,37 @@ void NDMService::handleSyncRequest(u32 messagePointer) {
}
void NDMService::overrideDefaultDaemons(u32 messagePointer) {
log("NDM::OverrideDefaultDaemons(stubbed)\n");
log("NDM::OverrideDefaultDaemons (stubbed)\n");
mem.write32(messagePointer, IPC::responseHeader(0x14, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void NDMService::resumeDaemons(u32 messagePointer) {
log("NDM::resumeDaemons(stubbed)\n");
log("NDM::resumeDaemons (stubbed)\n");
mem.write32(messagePointer, IPC::responseHeader(0x7, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void NDMService::suspendDaemons(u32 messagePointer) {
log("NDM::SuspendDaemons(stubbed)\n");
log("NDM::SuspendDaemons (stubbed)\n");
mem.write32(messagePointer, IPC::responseHeader(0x6, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void NDMService::resumeScheduler(u32 messagePointer) {
log("NDM::ResumeScheduler(stubbed)\n");
log("NDM::ResumeScheduler (stubbed)\n");
mem.write32(messagePointer, IPC::responseHeader(0x9, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void NDMService::suspendScheduler(u32 messagePointer) {
log("NDM::SuspendScheduler(stubbed)\n");
log("NDM::SuspendScheduler (stubbed)\n");
mem.write32(messagePointer, IPC::responseHeader(0x8, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void NDMService::clearHalfAwakeMacFilter(u32 messagePointer) {
log("NDM::ClearHalfAwakeMacFilter (stubbed)\n");
mem.write32(messagePointer, IPC::responseHeader(0x17, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}

View file

@ -0,0 +1,15 @@
#include "ipc.hpp"
#include "services/news_u.hpp"
namespace NewsCommands {
enum : u32 {};
}
void NewsUService::reset() {}
void NewsUService::handleSyncRequest(u32 messagePointer) {
const u32 command = mem.read32(messagePointer);
switch (command) {
default: Helpers::panic("news:u service requested. Command: %08X\n", command);
}
}

View file

@ -5,22 +5,35 @@
namespace NFCCommands {
enum : u32 {
Initialize = 0x00010040,
StartCommunication = 0x00030000,
StopCommunication = 0x00040000,
GetTagInRangeEvent = 0x000B0000,
GetTagOutOfRangeEvent = 0x000C0000
GetTagOutOfRangeEvent = 0x000C0000,
GetTagState = 0x000D0000,
CommunicationGetStatus = 0x000F0000,
CommunicationGetResult = 0x00120000,
};
}
void NFCService::reset() {
tagInRangeEvent = std::nullopt;
tagOutOfRangeEvent = std::nullopt;
adapterStatus = Old3DSAdapterStatus::Idle;
tagStatus = TagStatus::NotInitialized;
initialized = false;
}
void NFCService::handleSyncRequest(u32 messagePointer) {
const u32 command = mem.read32(messagePointer);
switch (command) {
case NFCCommands::CommunicationGetStatus: communicationGetStatus(messagePointer); break;
case NFCCommands::Initialize: initialize(messagePointer); break;
case NFCCommands::GetTagInRangeEvent: getTagInRangeEvent(messagePointer); break;
case NFCCommands::GetTagOutOfRangeEvent: getTagOutOfRangeEvent(messagePointer); break;
case NFCCommands::GetTagState: getTagState(messagePointer); break;
case NFCCommands::StartCommunication: startCommunication(messagePointer); break;
case NFCCommands::StopCommunication: stopCommunication(messagePointer); break;
default: Helpers::panic("NFC service requested. Command: %08X\n", command);
}
}
@ -29,6 +42,9 @@ void NFCService::initialize(u32 messagePointer) {
const u8 type = mem.read8(messagePointer + 4);
log("NFC::Initialize (type = %d)\n", type);
adapterStatus = Old3DSAdapterStatus::InitializationComplete;
tagStatus = TagStatus::Initialized;
initialized = true;
// TODO: This should error if already initialized. Also sanitize type.
mem.write32(messagePointer, IPC::responseHeader(0x1, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
@ -67,4 +83,42 @@ void NFCService::getTagOutOfRangeEvent(u32 messagePointer) {
mem.write32(messagePointer + 4, Result::Success);
// TODO: Translation descriptor here
mem.write32(messagePointer + 12, tagOutOfRangeEvent.value());
}
void NFCService::getTagState(u32 messagePointer) {
log("NFC::GetTagState");
mem.write32(messagePointer, IPC::responseHeader(0xD, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write8(messagePointer + 8, static_cast<u8>(tagStatus));
}
void NFCService::communicationGetStatus(u32 messagePointer) {
log("NFC::CommunicationGetStatus");
if (!initialized) {
Helpers::warn("NFC::CommunicationGetStatus: Old 3DS NFC Adapter not initialized\n");
}
mem.write32(messagePointer, IPC::responseHeader(0xF, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write8(messagePointer + 8, static_cast<u32>(adapterStatus));
}
void NFCService::startCommunication(u32 messagePointer) {
log("NFC::StartCommunication\n");
// adapterStatus = Old3DSAdapterStatus::Active;
// TODO: Actually start communication when we emulate amiibo
mem.write32(messagePointer, IPC::responseHeader(0x3, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void NFCService::stopCommunication(u32 messagePointer) {
log("NFC::StopCommunication\n");
adapterStatus = Old3DSAdapterStatus::InitializationComplete;
// TODO: Actually stop communication when we emulate amiibo
mem.write32(messagePointer, IPC::responseHeader(0x4, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}

View file

@ -3,9 +3,11 @@
namespace PTMCommands {
enum : u32 {
GetAdapterState = 0x00050000,
GetBatteryLevel = 0x00070000,
GetStepHistory = 0x000B00C2,
GetTotalStepCount = 0x000C0000,
ConfigureNew3DSCPU = 0x08180040
ConfigureNew3DSCPU = 0x08180040,
};
}
@ -15,12 +17,30 @@ void PTMService::handleSyncRequest(u32 messagePointer) {
const u32 command = mem.read32(messagePointer);
switch (command) {
case PTMCommands::ConfigureNew3DSCPU: configureNew3DSCPU(messagePointer); break;
case PTMCommands::GetAdapterState: getAdapterState(messagePointer); break;
case PTMCommands::GetBatteryLevel: getBatteryLevel(messagePointer); break;
case PTMCommands::GetStepHistory: getStepHistory(messagePointer); break;
case PTMCommands::GetTotalStepCount: getTotalStepCount(messagePointer); break;
default: Helpers::panic("PTM service requested. Command: %08X\n", command);
}
}
void PTMService::getAdapterState(u32 messagePointer) {
log("PTM::GetAdapterState\n");
mem.write32(messagePointer, IPC::responseHeader(0x5, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write8(messagePointer + 8, config.chargerPlugged ? 1 : 0);
}
void PTMService::getBatteryLevel(u32 messagePointer) {
log("PTM::GetBatteryLevel");
mem.write32(messagePointer, IPC::responseHeader(0x7, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write8(messagePointer + 8, batteryPercentToLevel(config.batteryPercentage));
}
void PTMService::getStepHistory(u32 messagePointer) {
log("PTM::GetStepHistory [stubbed]\n");
mem.write32(messagePointer, IPC::responseHeader(0xB, 1, 2));

View file

@ -5,10 +5,11 @@
#include "ipc.hpp"
#include "kernel.hpp"
ServiceManager::ServiceManager(std::span<u32, 16> regs, Memory& mem, GPU& gpu, u32& currentPID, Kernel& kernel)
: regs(regs), mem(mem), kernel(kernel), ac(mem), am(mem), boss(mem), act(mem), apt(mem, kernel), cam(mem), cecd(mem, kernel), cfg(mem),
ServiceManager::ServiceManager(std::span<u32, 16> regs, Memory& mem, GPU& gpu, u32& currentPID, Kernel& kernel, const EmulatorConfig& config)
: regs(regs), mem(mem), kernel(kernel), ac(mem), am(mem), boss(mem), act(mem), apt(mem, kernel), cam(mem, kernel), cecd(mem, kernel), cfg(mem),
dlp_srvr(mem), dsp(mem, kernel), hid(mem, kernel), http(mem), ir_user(mem, kernel), frd(mem), fs(mem, kernel),
gsp_gpu(mem, gpu, kernel, currentPID), gsp_lcd(mem), ldr(mem), mic(mem), nfc(mem, kernel), nim(mem), ndm(mem), ptm(mem), y2r(mem, kernel) {}
gsp_gpu(mem, gpu, kernel, currentPID), gsp_lcd(mem), ldr(mem), mcu_hwc(mem, config), mic(mem), nfc(mem, kernel), nim(mem), ndm(mem),
news_u(mem), ptm(mem, config), soc(mem), ssl(mem), y2r(mem, kernel) {}
static constexpr int MAX_NOTIFICATION_COUNT = 16;
@ -31,11 +32,16 @@ void ServiceManager::reset() {
fs.reset();
gsp_gpu.reset();
gsp_lcd.reset();
ldr.reset();
ldr.reset();
mcu_hwc.reset();
mic.reset();
nim.reset();
ndm.reset();
news_u.reset();
nfc.reset();
nim.reset();
ptm.reset();
soc.reset();
ssl.reset();
y2r.reset();
notificationSemaphore = std::nullopt;
@ -95,7 +101,8 @@ static std::map<std::string, Handle> serviceMap = {
{ "boss:U", KernelHandles::BOSS },
{ "cam:u", KernelHandles::CAM },
{ "cecd:u", KernelHandles::CECD },
{ "cfg:u", KernelHandles::CFG },
{ "cfg:u", KernelHandles::CFG_U },
{ "cfg:i", KernelHandles::CFG_I },
{ "dlp:SRVR", KernelHandles::DLP_SRVR },
{ "dsp::DSP", KernelHandles::DSP },
{ "hid:USER", KernelHandles::HID },
@ -106,12 +113,16 @@ static std::map<std::string, Handle> serviceMap = {
{ "gsp::Gpu", KernelHandles::GPU },
{ "gsp::Lcd", KernelHandles::LCD },
{ "ldr:ro", KernelHandles::LDR_RO },
{ "mcu::HWC", KernelHandles::MCU_HWC },
{ "mic:u", KernelHandles::MIC },
{ "ndm:u", KernelHandles::NDM },
{ "news:u", KernelHandles::NEWS_U },
{ "nfc:u", KernelHandles::NFC },
{ "nim:aoc", KernelHandles::NIM },
{ "ptm:u", KernelHandles::PTM }, // TODO: ptm:u and ptm:sysm have very different command sets
{ "ptm:sysm", KernelHandles::PTM },
{ "soc:U", KernelHandles::SOC },
{ "ssl:C", KernelHandles::SSL },
{ "y2r:u", KernelHandles::Y2R }
};
// clang-format on
@ -175,25 +186,29 @@ void ServiceManager::sendCommandToService(u32 messagePointer, Handle handle) {
case KernelHandles::APT: [[likely]] apt.handleSyncRequest(messagePointer); break;
case KernelHandles::DSP: [[likely]] dsp.handleSyncRequest(messagePointer); break;
case KernelHandles::AC: ac.handleSyncRequest(messagePointer); break;
case KernelHandles::AC: ac.handleSyncRequest(messagePointer); break;
case KernelHandles::ACT: act.handleSyncRequest(messagePointer); break;
case KernelHandles::AM: am.handleSyncRequest(messagePointer); break;
case KernelHandles::BOSS: boss.handleSyncRequest(messagePointer); break;
case KernelHandles::AM: am.handleSyncRequest(messagePointer); break;
case KernelHandles::BOSS: boss.handleSyncRequest(messagePointer); break;
case KernelHandles::CAM: cam.handleSyncRequest(messagePointer); break;
case KernelHandles::CECD: cecd.handleSyncRequest(messagePointer); break;
case KernelHandles::CFG: cfg.handleSyncRequest(messagePointer); break;
case KernelHandles::CFG_U: cfg.handleSyncRequest(messagePointer); break;
case KernelHandles::DLP_SRVR: dlp_srvr.handleSyncRequest(messagePointer); break;
case KernelHandles::HID: hid.handleSyncRequest(messagePointer); break;
case KernelHandles::HTTP: http.handleSyncRequest(messagePointer); break;
case KernelHandles::IR_USER: ir_user.handleSyncRequest(messagePointer); break;
case KernelHandles::FRD: frd.handleSyncRequest(messagePointer); break;
case KernelHandles::FRD: frd.handleSyncRequest(messagePointer); break;
case KernelHandles::LCD: gsp_lcd.handleSyncRequest(messagePointer); break;
case KernelHandles::LDR_RO: ldr.handleSyncRequest(messagePointer); break;
case KernelHandles::LDR_RO: ldr.handleSyncRequest(messagePointer); break;
case KernelHandles::MCU_HWC: mcu_hwc.handleSyncRequest(messagePointer); break;
case KernelHandles::MIC: mic.handleSyncRequest(messagePointer); break;
case KernelHandles::NFC: nfc.handleSyncRequest(messagePointer); break;
case KernelHandles::NIM: nim.handleSyncRequest(messagePointer); break;
case KernelHandles::NIM: nim.handleSyncRequest(messagePointer); break;
case KernelHandles::NDM: ndm.handleSyncRequest(messagePointer); break;
case KernelHandles::NEWS_U: news_u.handleSyncRequest(messagePointer); break;
case KernelHandles::PTM: ptm.handleSyncRequest(messagePointer); break;
case KernelHandles::SOC: soc.handleSyncRequest(messagePointer); break;
case KernelHandles::SSL: ssl.handleSyncRequest(messagePointer); break;
case KernelHandles::Y2R: y2r.handleSyncRequest(messagePointer); break;
default: Helpers::panic("Sent IPC message to unknown service %08X\n Command: %08X", handle, mem.read32(messagePointer));
}

File diff suppressed because it is too large Load diff

33
src/core/services/soc.cpp Normal file
View file

@ -0,0 +1,33 @@
#include "services/soc.hpp"
#include "ipc.hpp"
#include "result/result.hpp"
namespace SOCCommands {
enum : u32 {
InitializeSockets = 0x00010044,
};
}
void SOCService::reset() { initialized = false; }
void SOCService::handleSyncRequest(u32 messagePointer) {
const u32 command = mem.read32(messagePointer);
switch (command) {
case SOCCommands::InitializeSockets: initializeSockets(messagePointer); break;
default: Helpers::panic("SOC service requested. Command: %08X\n", command);
}
}
void SOCService::initializeSockets(u32 messagePointer) {
const u32 memoryBlockSize = mem.read32(messagePointer + 4);
const Handle sharedMemHandle = mem.read32(messagePointer + 20);
log("SOC::InitializeSockets (memory block size = %08X, shared mem handle = %08X)\n", memoryBlockSize, sharedMemHandle);
// TODO: Does double initialization return an error code?
// TODO: Implement the rest of this stuff when it's time to do online. Also implement error checking for the size, shared mem handle, and so on
initialized = true;
mem.write32(messagePointer, IPC::responseHeader(0x01, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}

63
src/core/services/ssl.cpp Normal file
View file

@ -0,0 +1,63 @@
#include "ipc.hpp"
#include "result/result.hpp"
#include "services/ssl.hpp"
namespace SSLCommands {
enum : u32 {
Initialize = 0x00010002,
GenerateRandomData = 0x00110042,
};
}
void SSLService::reset() {
initialized = false;
// Use the default seed on reset to avoid funny bugs
rng.seed();
}
void SSLService::handleSyncRequest(u32 messagePointer) {
const u32 command = mem.read32(messagePointer);
switch (command) {
case SSLCommands::Initialize: initialize(messagePointer); break;
case SSLCommands::GenerateRandomData: generateRandomData(messagePointer); break;
default: Helpers::panic("SSL service requested. Command: %08X\n", command);
}
}
void SSLService::initialize(u32 messagePointer) {
log("SSL::Initialize\n");
mem.write32(messagePointer, IPC::responseHeader(0x01, 1, 0));
if (initialized) {
Helpers::warn("SSL service initialized twice");
}
initialized = true;
rng.seed(std::random_device()()); // Seed rng via std::random_device
mem.write32(messagePointer + 4, Result::Success);
}
void SSLService::generateRandomData(u32 messagePointer) {
const u32 size = mem.read32(messagePointer + 4);
const u32 output = mem.read32(messagePointer + 12);
log("SSL::GenerateRandomData (out = %08X, size = %08X)\n", output, size);
// TODO: This might be a biiit slow, might want to make it write in word quantities
u32 data;
for (u32 i = 0; i < size; i++) {
// We don't have an available random value since we're on a multiple of 4 bytes and our Twister is 32-bit, generate a new one from the Mersenne Twister
if ((i & 3) == 0) {
data = rng();
}
mem.write8(output + i, u8(data));
// Shift data by 8 to get the next byte
data >>= 8;
}
mem.write32(messagePointer, IPC::responseHeader(0x11, 1, 2));
mem.write32(messagePointer + 4, Result::Success);
}

View file

@ -23,6 +23,7 @@ namespace Y2RCommands {
StartConversion = 0x00260000,
StopConversion = 0x00270000,
IsBusyConversion = 0x00280000,
SetPackageParameter = 0x002901C0,
PingProcess = 0x002A0000,
DriverInitialize = 0x002B0000,
DriverFinalize = 0x002C0000
@ -60,6 +61,7 @@ void Y2RService::handleSyncRequest(u32 messagePointer) {
case Y2RCommands::SetInputLineWidth: setInputLineWidth(messagePointer); break;
case Y2RCommands::SetInputLines: setInputLines(messagePointer); break;
case Y2RCommands::SetOutputFormat: setOutputFormat(messagePointer); break;
case Y2RCommands::SetPackageParameter: setPackageParameter(messagePointer); break;
case Y2RCommands::SetReceiving: setReceiving(messagePointer); break;
case Y2RCommands::SetRotation: setRotation(messagePointer); break;
case Y2RCommands::SetSendingY: setSendingY(messagePointer); break;
@ -176,6 +178,17 @@ void Y2RService::setOutputFormat(u32 messagePointer) {
mem.write32(messagePointer + 4, Result::Success);
}
void Y2RService::setPackageParameter(u32 messagePointer) {
// Package parameter is 3 words
const u32 word1 = mem.read32(messagePointer + 4);
const u32 word2 = mem.read32(messagePointer + 8);
const u32 word3 = mem.read32(messagePointer + 12);
Helpers::warn("Y2R::SetPackageParameter\n");
mem.write32(messagePointer, IPC::responseHeader(0x29, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void Y2RService::setRotation(u32 messagePointer) {
const u32 rot = mem.read32(messagePointer + 4);
log("Y2R::SetRotation (format = %d)\n", rot);

View file

@ -12,8 +12,8 @@ __declspec(dllexport) DWORD AmdPowerXpressRequestHighPerformance = 1;
#endif
Emulator::Emulator()
: config(std::filesystem::current_path() / "config.toml"), kernel(cpu, memory, gpu), cpu(memory, kernel), gpu(memory, config),
memory(cpu.getTicksRef()), cheats(memory, kernel.getServiceManager().getHID()), running(false), programRunning(false)
: config(std::filesystem::current_path() / "config.toml"), kernel(cpu, memory, gpu, config), cpu(memory, kernel), gpu(memory, config),
memory(cpu.getTicksRef(), config), cheats(memory, kernel.getServiceManager().getHID()), running(false), programRunning(false)
#ifdef PANDA3DS_ENABLE_HTTP_SERVER
, httpServer(this)
#endif
@ -316,19 +316,36 @@ void Emulator::run() {
// Detect mouse motion events for gyroscope emulation
case SDL_MOUSEMOTION: {
if (romType == ROMType::None) break;
// Handle "dragging" across the touchscreen
if (hid.isTouchScreenPressed()) {
const s32 x = event.motion.x;
const s32 y = event.motion.y;
// Check if touch falls in the touch screen area and register the new touch screen position
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;
hid.setTouchScreenPress(x_converted, y_converted);
}
}
// We use right click to indicate we want to rotate the console. If right click is not held, then this is not a gyroscope rotation
if (romType == ROMType::None || !holdingRightClick) break;
if (holdingRightClick) {
// Relative motion since last mouse motion event
const s32 motionX = event.motion.xrel;
const s32 motionY = event.motion.yrel;
// Relative motion since last mouse motion event
const s32 motionX = event.motion.xrel;
const s32 motionY = event.motion.yrel;
// The gyroscope involves lots of weird math I don't want to bother with atm
// So up until then, we will set the gyroscope euler angles to fixed values based on the direction of the relative motion
const s32 roll = motionX > 0 ? 0x7f : -0x7f;
const s32 pitch = motionY > 0 ? 0x7f : -0x7f;
hid.setRoll(roll);
hid.setPitch(pitch);
// The gyroscope involves lots of weird math I don't want to bother with atm
// So up until then, we will set the gyroscope euler angles to fixed values based on the direction of the relative motion
const s32 roll = motionX > 0 ? 0x7f : -0x7f;
const s32 pitch = motionY > 0 ? 0x7f : -0x7f;
hid.setRoll(roll);
hid.setPitch(pitch);
}
break;
}

View file

@ -232,8 +232,8 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) {
// Positional Light
if (bitfieldExtract(GPUREG_LIGHTi_CONFIG, 0, 1) == 0) {
error_unimpl = true;
// half_vector = normalize(normalize(light_vector + v_view) + view);
// error_unimpl = true;
half_vector = normalize(normalize(light_vector + v_view) + view);
}
// Directional light
@ -328,7 +328,7 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) {
if (fresnel_output2 == 1u) secondary_color.a = d[FR_LUT];
if (error_unimpl) {
secondary_color = primary_color = vec4(1.0, 0., 1.0, 1.0);
// secondary_color = primary_color = vec4(1.0, 0., 1.0, 1.0);
}
}

View file

@ -3,7 +3,8 @@
#include <algorithm>
#include <unordered_map>
Renderer::Renderer(GPU& gpu, const std::array<u32, regNum>& internalRegs) : gpu(gpu), regs(internalRegs) {}
Renderer::Renderer(GPU& gpu, const std::array<u32, regNum>& internalRegs, const std::array<u32, extRegNum>& externalRegs)
: gpu(gpu), regs(internalRegs), externalRegs(externalRegs) {}
Renderer::~Renderer() {}
std::optional<RendererType> Renderer::typeFromString(std::string inString) {