Merge branch 'master' into vfp-timing

This commit is contained in:
wheremyfoodat 2023-08-21 19:00:41 +03:00
commit 639ef614eb
157 changed files with 6820 additions and 263696 deletions

View file

@ -1,6 +1,8 @@
#include "config.hpp"
#include <cmath>
#include <fstream>
#include <string>
#include "helpers.hpp"
#include "toml.hpp"
@ -9,6 +11,8 @@
// We are legally allowed, as per the author's wish, to use the above code without any licensing restrictions
// However we still want to follow the license as closely as possible and offer the proper attributions.
EmulatorConfig::EmulatorConfig(const std::filesystem::path& path) { load(path); }
void EmulatorConfig::load(const std::filesystem::path& path) {
// If the configuration file does not exist, create it and return
std::error_code error;
@ -26,12 +30,55 @@ void EmulatorConfig::load(const std::filesystem::path& path) {
return;
}
if (data.contains("General")) {
auto generalResult = toml::expect<toml::value>(data.at("General"));
if (generalResult.is_ok()) {
auto general = generalResult.unwrap();
discordRpcEnabled = toml::find_or<toml::boolean>(general, "EnableDiscordRPC", false);
}
}
if (data.contains("GPU")) {
auto gpuResult = toml::expect<toml::value>(data.at("GPU"));
if (gpuResult.is_ok()) {
auto gpu = gpuResult.unwrap();
shaderJitEnabled = toml::find_or<toml::boolean>(gpu, "EnableShaderJIT", false);
// Get renderer
auto rendererName = toml::find_or<std::string>(gpu, "Renderer", "OpenGL");
auto configRendererType = Renderer::typeFromString(rendererName);
if (configRendererType.has_value()) {
rendererType = configRendererType.value();
} else {
Helpers::warn("Invalid renderer specified: %s\n", rendererName.c_str());
rendererType = RendererType::OpenGL;
}
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);
}
}
}
@ -43,7 +90,7 @@ void EmulatorConfig::save(const std::filesystem::path& path) {
if (std::filesystem::exists(path, error)) {
try {
data = toml::parse<toml::preserve_comments>(path);
} catch (std::exception& ex) {
} catch (const std::exception& ex) {
Helpers::warn("Exception trying to parse config file. Exception: %s\n", ex.what());
return;
}
@ -54,7 +101,15 @@ void EmulatorConfig::save(const std::filesystem::path& path) {
printf("Saving new configuration file %s\n", path.string().c_str());
}
data["General"]["EnableDiscordRPC"] = discordRpcEnabled;
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;

View file

@ -2,7 +2,7 @@
#include "cpu_dynarmic.hpp"
#include "arm_defs.hpp"
CPU::CPU(Memory& mem, Kernel& kernel) : mem(mem), env(mem, kernel, *this) {
CPU::CPU(Memory& mem, Kernel& kernel) : mem(mem), env(mem, kernel) {
cp15 = std::make_shared<CP15>();
Dynarmic::A32::UserConfig config;

View file

@ -143,6 +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:
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;
@ -179,6 +182,10 @@ void ShaderEmitter::compileInstruction(const PICAShader& shaderUnit) {
case ShaderOpcodes::SLTI:
recSLT(shaderUnit, instruction); break;
case ShaderOpcodes::SGE:
case ShaderOpcodes::SGEI:
recSGE(shaderUnit, instruction); break;
default:
Helpers::panic("Shader JIT: Unimplemented PICA opcode %X", opcode);
}
@ -525,6 +532,32 @@ void ShaderEmitter::recDP4(const PICAShader& shader, u32 instruction) {
storeRegister(src1_xmm, shader, dest, operandDescriptor);
}
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 = 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, 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) {
blendps(src1_xmm, xword[rip + onesVector], 0b1000);
} else {
movaps(scratch1, src1_xmm);
unpckhps(scratch1, xword[rip + onesVector]);
unpcklpd(src1_xmm, scratch1);
}
dpps(src1_xmm, src2_xmm, 0b11111111); // 4-lane dot product between the 2 registers, store the result in all lanes of scratch1 similarly to PICA
storeRegister(src1_xmm, shader, dest, operandDescriptor);
}
void ShaderEmitter::recMAX(const PICAShader& shader, u32 instruction) {
const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f];
const u32 src1 = getBits<12, 7>(instruction);
@ -656,6 +689,24 @@ void ShaderEmitter::recSLT(const PICAShader& shader, u32 instruction) {
storeRegister(src1_xmm, shader, dest, operandDescriptor);
}
void ShaderEmitter::recSGE(const PICAShader& shader, u32 instruction) {
const bool isSGEI = (instruction >> 26) == ShaderOpcodes::SGEI;
const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f];
const u32 src1 = isSGEI ? getBits<14, 5>(instruction) : getBits<12, 7>(instruction);
const u32 src2 = isSGEI ? getBits<7, 7>(instruction) : getBits<7, 5>(instruction);
const u32 idx = getBits<19, 2>(instruction);
const u32 dest = getBits<21, 5>(instruction);
loadRegister<1>(src1_xmm, shader, src1, isSGEI ? 0 : idx, operandDescriptor);
loadRegister<2>(src2_xmm, shader, src2, isSGEI ? idx : 0, operandDescriptor);
// SSE does not have a cmpgeps instruction so we turn src1 >= src2 to src2 <= src1, result in src2
cmpleps(src2_xmm, src1_xmm);
andps(src2_xmm, xword[rip + onesVector]);
storeRegister(src2_xmm, shader, dest, operandDescriptor);
}
void ShaderEmitter::recCMP(const PICAShader& shader, u32 instruction) {
const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f];
const u32 src1 = getBits<12, 7>(instruction);

View file

@ -7,10 +7,20 @@
#include "PICA/float_types.hpp"
#include "PICA/regs.hpp"
#include "renderer_null/renderer_null.hpp"
#include "renderer_sw/renderer_sw.hpp"
#ifdef PANDA3DS_ENABLE_OPENGL
#include "renderer_gl/renderer_gl.hpp"
#endif
#ifdef PANDA3DS_ENABLE_VULKAN
#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;
@ -20,10 +30,34 @@ GPU::GPU(Memory& mem, EmulatorConfig& config) : mem(mem), config(config) {
vram = new u8[vramSize];
mem.setVRAM(vram); // Give the bus a pointer to our VRAM
// TODO: Configurable backend
switch (config.rendererType) {
case RendererType::Null: {
renderer.reset(new RendererNull(*this, regs, externalRegs));
break;
}
case RendererType::Software: {
renderer.reset(new RendererSw(*this, regs, externalRegs));
break;
}
#ifdef PANDA3DS_ENABLE_OPENGL
renderer.reset(new RendererGL(*this, regs));
case RendererType::OpenGL: {
renderer.reset(new RendererGL(*this, regs, externalRegs));
break;
}
#endif
#ifdef PANDA3DS_ENABLE_VULKAN
case RendererType::Vulkan: {
renderer.reset(new RendererVK(*this, regs, externalRegs));
break;
}
#endif
default: {
Helpers::panic("Rendering backend not supported: %s", Renderer::typeToString(config.rendererType));
break;
}
}
}
void GPU::reset() {
@ -50,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();
}
@ -293,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);

235
src/core/action_replay.cpp Normal file
View file

@ -0,0 +1,235 @@
#include "action_replay.hpp"
ActionReplay::ActionReplay(Memory& mem, HIDService& hid) : mem(mem), hid(hid) { reset(); }
void ActionReplay::reset() {
// Default value of storage regs is 0
storage1 = 0;
storage2 = 0;
// TODO: Is the active storage persistent or not?
activeStorage = &storage1;
}
void ActionReplay::runCheat(const Cheat& cheat) {
// Set offset and data registers to 0 at the start of a cheat
data1 = data2 = offset1 = offset2 = 0;
pc = 0;
ifStackIndex = 0;
loopStackIndex = 0;
running = true;
activeOffset = &offset1;
activeData = &data1;
while (running) {
// See if we can fetch 1 64-bit opcode, otherwise we're out of bounds. Cheats seem to end when going out of bounds?
if (pc + 1 >= cheat.size()) {
return;
}
// Fetch instruction
const u32 instruction = cheat[pc++];
// Instructions D0000000 00000000 and D2000000 00000000 are unconditional
bool isUnconditional = cheat[pc] == 0 && (instruction == 0xD0000000 || instruction == 0xD2000000);
if (ifStackIndex > 0 && !isUnconditional && !ifStack[ifStackIndex - 1]) {
pc++; // Eat up dummy word
continue; // Skip conditional instructions where the condition is false
}
runInstruction(cheat, instruction);
}
}
u8 ActionReplay::read8(u32 addr) { return mem.read8(addr); }
u16 ActionReplay::read16(u32 addr) { return mem.read16(addr); }
u32 ActionReplay::read32(u32 addr) { return mem.read32(addr); }
// Some AR cheats seem to want to write to unmapped memory or memory that straight up does not exist
#define MAKE_WRITE_HANDLER(size) \
void ActionReplay::write##size(u32 addr, u##size value) { \
auto pointerWrite = mem.getWritePointer(addr); \
if (pointerWrite) { \
*(u##size*)pointerWrite = value; \
} else { \
auto pointerRead = mem.getReadPointer(addr); \
if (pointerRead) { \
*(u##size*)pointerRead = value; \
} else { \
Helpers::warn("AR code tried to write to invalid address: %08X\n", addr); \
} \
} \
}
MAKE_WRITE_HANDLER(8)
MAKE_WRITE_HANDLER(16)
MAKE_WRITE_HANDLER(32)
#undef MAKE_WRITE_HANDLER
void ActionReplay::runInstruction(const Cheat& cheat, u32 instruction) {
// Top nibble determines the instruction type
const u32 type = instruction >> 28;
switch (type) {
// 32-bit write to [XXXXXXX + offset]
case 0x0: {
const u32 baseAddr = Helpers::getBits<0, 28>(instruction);
const u32 value = cheat[pc++];
write32(baseAddr + *activeOffset, value);
break;
}
// 16-bit write to [XXXXXXX + offset]
case 0x1: {
const u32 baseAddr = Helpers::getBits<0, 28>(instruction);
const u16 value = u16(cheat[pc++]);
write16(baseAddr + *activeOffset, value);
break;
}
// 8-bit write to [XXXXXXX + offset]
case 0x2: {
const u32 baseAddr = Helpers::getBits<0, 28>(instruction);
const u8 value = u8(cheat[pc++]);
write8(baseAddr + *activeOffset, value);
break;
}
// clang-format off
#define MAKE_IF_INSTRUCTION(opcode, comparator) \
case opcode: { \
const u32 baseAddr = Helpers::getBits<0, 28>(instruction); \
const u32 imm = cheat[pc++]; \
const u32 value = read32(baseAddr + *activeOffset); \
\
pushConditionBlock(imm comparator value); \
break; \
}
// Greater Than (YYYYYYYY > [XXXXXXX + offset]) (Unsigned)
MAKE_IF_INSTRUCTION(3, >)
// Less Than (YYYYYYYY < [XXXXXXX + offset]) (Unsigned)
MAKE_IF_INSTRUCTION(4, <)
// Equal to (YYYYYYYY == [XXXXXXX + offset])
MAKE_IF_INSTRUCTION(5, ==)
// Not Equal (YYYYYYYY != [XXXXXXX + offset])
MAKE_IF_INSTRUCTION(6, !=)
#undef MAKE_IF_INSTRUCTION
// clang-format on
// BXXXXXXX 00000000 - offset = *(XXXXXXX + offset)
case 0xB: {
const u32 baseAddr = Helpers::getBits<0, 28>(instruction);
*activeOffset = read32(baseAddr + *activeOffset);
pc++; // Eat up dummy word
break;
}
case 0xD: executeDType(cheat, instruction); break;
default: Helpers::panic("Unimplemented ActionReplay instruction type %X", type); break;
}
}
void ActionReplay::executeDType(const Cheat& cheat, u32 instruction) {
switch (instruction) {
case 0xD3000000: offset1 = cheat[pc++]; break;
case 0xD3000001: offset2 = cheat[pc++]; break;
case 0xDC000000: *activeOffset += cheat[pc++]; break;
// DD000000 XXXXXXXX - if KEYPAD has value XXXXXXXX execute next block
case 0xDD000000: {
const u32 mask = cheat[pc++];
const u32 buttons = hid.getOldButtons();
pushConditionBlock((buttons & mask) == mask);
break;
}
// Offset register ops
case 0xDF000000: {
const u32 subopcode = cheat[pc++];
switch (subopcode) {
case 0x00000000: activeOffset = &offset1; break;
case 0x00000001: activeOffset = &offset2; break;
case 0x00010000: offset2 = offset1; break;
case 0x00010001: offset1 = offset2; break;
case 0x00020000: data1 = offset1; break;
case 0x00020001: data2 = offset2; break;
default:
Helpers::warn("Unknown ActionReplay offset operation");
running = false;
break;
}
break;
}
// Data register operations
case 0xDF000001: {
const u32 subopcode = cheat[pc++];
switch (subopcode) {
case 0x00000000: activeData = &data1; break;
case 0x00000001: activeData = &data2; break;
case 0x00010000: data2 = data1; break;
case 0x00010001: data1 = data2; break;
case 0x00020000: offset1 = data1; break;
case 0x00020001: offset2 = data2; break;
default:
Helpers::warn("Unknown ActionReplay data operation");
running = false;
break;
}
break;
}
// Storage register operations
case 0xDF000002: {
const u32 subopcode = cheat[pc++];
switch (subopcode) {
case 0x00000000: activeStorage = &storage1; break;
case 0x00000001: activeStorage = &storage2; break;
case 0x00010000: data1 = storage1; break;
case 0x00010001: data2 = storage2; break;
case 0x00020000: storage1 = data1; break;
case 0x00020001: storage2 = data2; break;
default:
Helpers::warn("Unknown ActionReplay data operation: %08X", subopcode);
running = false;
break;
}
break;
}
// Control flow block operations
case 0xD2000000: {
const u32 subopcode = cheat[pc++];
switch (subopcode) {
// Ends all loop/execute blocks
case 0:
loopStackIndex = 0;
ifStackIndex = 0;
break;
default: Helpers::panic("Unknown ActionReplay control flow operation: %08X", subopcode); break;
}
break;
}
default: Helpers::panic("ActionReplay: Unimplemented d-type opcode: %08X", instruction); break;
}
}
void ActionReplay::pushConditionBlock(bool condition) {
if (ifStackIndex >= 32) {
Helpers::warn("ActionReplay if stack overflowed");
running = false;
return;
}
ifStack[ifStackIndex++] = condition;
}

31
src/core/cheats.cpp Normal file
View file

@ -0,0 +1,31 @@
#include "cheats.hpp"
Cheats::Cheats(Memory& mem, HIDService& hid) : ar(mem, hid) { reset(); }
void Cheats::reset() {
clear(); // Clear loaded cheats
ar.reset(); // Reset ActionReplay
}
void Cheats::addCheat(const Cheat& cheat) {
cheats.push_back(cheat);
cheatsLoaded = true;
}
void Cheats::clear() {
cheats.clear();
cheatsLoaded = false;
}
void Cheats::run() {
for (const Cheat& cheat : cheats) {
switch (cheat.type) {
case CheatType::ActionReplay: {
ar.runCheat(cheat.instructions);
break;
}
default: Helpers::panic("Unknown cheat device!");
}
}
}

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;
}
@ -156,6 +159,8 @@ Rust::Result<ArchiveBase::FormatInfo, HorizonResult> SaveDataArchive::getFormatI
FormatInfo ret;
auto [success, bytesRead] = file.readBytes(&ret, sizeof(FormatInfo));
file.close();
if (!success || bytesRead != sizeof(FormatInfo)) {
Helpers::warn("SaveData::GetFormatInfo: Format file exists but was not properly read into the FormatInfo struct");
return Err(Result::FS::NotFormatted);
@ -175,6 +180,8 @@ void SaveDataArchive::format(const FSPath& path, const ArchiveBase::FormatInfo&
// Write format info on disk
IOFile file(formatInfoPath, "wb");
file.writeBytes(&info, sizeof(info));
file.flush();
file.close();
}
Rust::Result<ArchiveBase*, HorizonResult> SaveDataArchive::openArchive(const FSPath& path) {

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]);

76
src/core/fs/ivfc.cpp Normal file
View file

@ -0,0 +1,76 @@
#include "fs/ivfc.hpp"
namespace IVFC {
size_t parseIVFC(uintptr_t ivfcStart, IVFC& ivfc) {
uintptr_t ivfcPointer = ivfcStart;
char* ivfcCharPtr = (char*)ivfcPointer;
if (ivfcCharPtr[0] != 'I' || ivfcCharPtr[1] != 'V' || ivfcCharPtr[2] != 'F' || ivfcCharPtr[3] != 'C') {
printf("Invalid header on IVFC\n");
return 0;
}
ivfcPointer += 4;
u32 magicIdentifier = *(u32*)ivfcPointer;
ivfcPointer += 4;
// RomFS IVFC uses 0x10000, DISA/DIFF IVFC uses 0x20000 here
if (magicIdentifier != 0x10000 && magicIdentifier != 0x20000) {
printf("Invalid IVFC magic identifier: %08X\n", magicIdentifier);
return 0;
}
if (magicIdentifier == 0x10000) {
ivfc.masterHashSize = *(u32*)ivfcPointer;
ivfcPointer += 4;
// RomFS IVFC uses 3 levels
ivfc.levels.resize(3);
} else {
ivfc.masterHashSize = *(u64*)ivfcPointer;
ivfcPointer += 8;
// DISA/DIFF IVFC uses 4 levels
ivfc.levels.resize(4);
}
for (size_t i = 0; i < ivfc.levels.size(); i++) {
IVFCLevel level;
level.logicalOffset = *(u64*)ivfcPointer;
ivfcPointer += 8;
level.size = *(u64*)ivfcPointer;
ivfcPointer += 8;
// This field is in log2
level.blockSize = 1 << *(u32*)ivfcPointer;
ivfcPointer += 4;
// Skip 4 reserved bytes
ivfcPointer += 4;
ivfc.levels[i] = level;
}
u64 ivfcDescriptorSize = *(u64*)ivfcPointer;
ivfcPointer += 8;
uintptr_t ivfcActualSize = ivfcPointer - ivfcStart;
// According to 3DBrew, this is usually the case but not guaranteed
if (ivfcActualSize != ivfcDescriptorSize) {
printf("IVFC descriptor size mismatch: %llx != %llx\n", ivfcActualSize, ivfcDescriptorSize);
}
if (magicIdentifier == 0x10000 && ivfcActualSize != 0x5C) {
// This is always 0x5C bytes long
printf("Invalid IVFC size: %08x\n", (u32)ivfcActualSize);
return 0;
} else if (magicIdentifier == 0x20000 && ivfcActualSize != 0x78) {
// This is always 0x78 bytes long
printf("Invalid IVFC size: %08x\n", (u32)ivfcActualSize);
return 0;
}
return ivfcActualSize;
}
} // namespace IVFC

194
src/core/fs/romfs.cpp Normal file
View file

@ -0,0 +1,194 @@
#include "fs/romfs.hpp"
#include <cstdio>
#include <queue>
#include <string>
#include "fs/ivfc.hpp"
#include "helpers.hpp"
namespace RomFS {
constexpr u32 metadataInvalidEntry = 0xFFFFFFFF;
struct Level3Header {
u32 headerSize;
u32 directoryHashTableOffset;
u32 directoryHashTableSize;
u32 directoryMetadataOffset;
u32 directoryMetadataSize;
u32 fileHashTableOffset;
u32 fileHashTableSize;
u32 fileMetadataOffset;
u32 fileMetadataSize;
u32 fileDataOffset;
};
inline constexpr uintptr_t alignUp(uintptr_t value, uintptr_t alignment) {
if (value % alignment == 0) return value;
return value + (alignment - (value % alignment));
}
void printNode(const RomFSNode& node, int indentation) {
for (int i = 0; i < indentation; i++) {
printf(" ");
}
printf("%s/\n", std::string(node.name.begin(), node.name.end()).c_str());
for (auto& file : node.files) {
for (int i = 0; i <= indentation; i++) {
printf(" ");
}
printf("%s\n", std::string(file->name.begin(), file->name.end()).c_str());
}
indentation++;
for (auto& directory : node.directories) {
printNode(*directory, indentation);
}
indentation--;
}
std::vector<std::unique_ptr<RomFSNode>> getFiles(uintptr_t fileMetadataBase, u32 currentFileOffset) {
std::vector<std::unique_ptr<RomFSNode>> files;
while (currentFileOffset != metadataInvalidEntry) {
u32* metadataPtr = (u32*)(fileMetadataBase + currentFileOffset);
metadataPtr++; // Skip the containing directory
u32 nextFileOffset = *metadataPtr++;
u64 fileDataOffset = *(u64*)metadataPtr;
metadataPtr += 2;
u64 fileSize = *(u64*)metadataPtr;
metadataPtr += 2;
metadataPtr++; // Skip the offset of the next file in the same hash table bucket
u32 nameLength = *metadataPtr++ / 2;
// Arbitrary limit
if (nameLength > 128) {
printf("Invalid file name length: %08X\n", nameLength);
return {};
}
char16_t* namePtr = (char16_t*)metadataPtr;
std::u16string name(namePtr, nameLength);
std::unique_ptr file = std::make_unique<RomFSNode>();
file->isDirectory = false;
file->name = name;
file->metadataOffset = currentFileOffset;
file->dataOffset = fileDataOffset;
file->dataSize = fileSize;
files.push_back(std::move(file));
currentFileOffset = nextFileOffset;
}
return files;
}
std::unique_ptr<RomFSNode> parseRootDirectory(uintptr_t directoryMetadataBase, uintptr_t fileMetadataBase) {
std::unique_ptr<RomFSNode> rootDirectory = std::make_unique<RomFSNode>();
rootDirectory->isDirectory = true;
rootDirectory->name = u"romfs:";
rootDirectory->metadataOffset = 0;
u32 rootFilesOffset = *((u32*)(directoryMetadataBase) + 3);
if (rootFilesOffset != metadataInvalidEntry) {
rootDirectory->files = getFiles(fileMetadataBase, rootFilesOffset);
}
std::queue<RomFSNode*> directoryOffsets;
directoryOffsets.push(rootDirectory.get());
while (!directoryOffsets.empty()) {
RomFSNode* currentNode = directoryOffsets.front();
directoryOffsets.pop();
u32* metadataPtr = (u32*)(directoryMetadataBase + currentNode->metadataOffset);
metadataPtr += 2;
// Offset of first child directory
u32 currentDirectoryOffset = *metadataPtr;
// Loop over all the sibling directories of the first child to get all the children directories
// of the current directory
while (currentDirectoryOffset != metadataInvalidEntry) {
metadataPtr = (u32*)(directoryMetadataBase + currentDirectoryOffset);
metadataPtr++; // Skip the parent offset
u32 siblingDirectoryOffset = *metadataPtr++;
metadataPtr++; // Skip offset of first child directory
u32 currentFileOffset = *metadataPtr++;
metadataPtr++; // Skip offset of next directory in the same hash table bucket
u32 nameLength = *metadataPtr++ / 2;
// Arbitrary limit
if (nameLength > 128) {
printf("Invalid directory name length: %08X\n", nameLength);
return {};
}
char16_t* namePtr = (char16_t*)metadataPtr;
std::u16string name(namePtr, nameLength);
std::unique_ptr directory = std::make_unique<RomFSNode>();
directory->isDirectory = true;
directory->name = name;
directory->metadataOffset = currentDirectoryOffset;
directory->files = getFiles(fileMetadataBase, currentFileOffset);
currentNode->directories.push_back(std::move(directory));
currentDirectoryOffset = siblingDirectoryOffset;
}
for (auto& directory : currentNode->directories) {
directoryOffsets.push(directory.get());
}
}
return rootDirectory;
}
std::unique_ptr<RomFSNode> parseRomFSTree(uintptr_t romFS, u64 romFSSize) {
IVFC::IVFC ivfc;
size_t ivfcSize = IVFC::parseIVFC((uintptr_t)romFS, ivfc);
if (ivfcSize == 0) {
printf("Failed to parse IVFC\n");
return {};
}
uintptr_t masterHashOffset = RomFS::alignUp(ivfcSize, 0x10);
// From GBATEK:
// The "Logical Offsets" are completely unrelated to the physical offsets in the RomFS partition.
// Instead, the "Logical Offsets" might be something about where to map the Level 1-3 sections in
// virtual memory (with the physical Level 3,1,2 ordering being re-ordered to Level 1,2,3)?
uintptr_t level3Offset = RomFS::alignUp(masterHashOffset + ivfc.masterHashSize, ivfc.levels[2].blockSize);
uintptr_t level3Base = (uintptr_t)romFS + level3Offset;
u32* level3Ptr = (u32*)level3Base;
Level3Header header;
header.headerSize = *level3Ptr++;
header.directoryHashTableOffset = *level3Ptr++;
header.directoryHashTableSize = *level3Ptr++;
header.directoryMetadataOffset = *level3Ptr++;
header.directoryMetadataSize = *level3Ptr++;
header.fileHashTableOffset = *level3Ptr++;
header.fileHashTableSize = *level3Ptr++;
header.fileMetadataOffset = *level3Ptr++;
header.fileMetadataSize = *level3Ptr++;
header.fileDataOffset = *level3Ptr;
if (header.headerSize != 0x28) {
printf("Invalid level 3 header size: %08X\n", header.headerSize);
return {};
}
std::unique_ptr<RomFSNode> root = parseRootDirectory(level3Base + header.directoryMetadataOffset, level3Base + header.fileMetadataOffset);
// If you want to print the tree, uncomment this
// printNode(*root, 0);
return root;
}
} // namespace RomFS

View file

@ -87,7 +87,7 @@ void Kernel::arbitrateAddress() {
Helpers::panic("ArbitrateAddress: Unimplemented type %s", arbitrationTypeToString(type));
}
rescheduleThreads();
requireReschedule();
}
// Signal up to "threadCount" threads waiting on the arbiter indicated by "waitingAddress"

View file

@ -1,3 +1,10 @@
#include <array>
#include <cctype>
#include <filesystem>
#include <string>
#include <utility>
#include "ipc.hpp"
#include "kernel.hpp"
namespace DirectoryOps {
@ -7,6 +14,79 @@ namespace DirectoryOps {
};
}
// Helper to convert std::string to an 8.3 filename to mimic how Directory::Read works
using ShortFilename = std::array<char, 9>;
using ShortExtension = std::array<char, 4>;
using Filename83 = std::pair<ShortFilename, ShortExtension>;
// The input string should be the stem and extension together, not separately
// Eg something like "boop.png", "panda.txt", etc
Filename83 convertTo83(const std::string& path) {
ShortFilename filename;
ShortExtension extension;
// Convert a character to add it to the 8.3 name
// "Characters such as + are changed to the underscore _, and letters are put in uppercase"
// For now we put letters in uppercase until we find out what is supposed to be converted to _ and so on
auto convertCharacter = [](char c) { return (char) std::toupper(c); };
// List of forbidden character for 8.3 filenames, from Citra
// TODO: Use constexpr when C++20 support is solid
const std::string forbiddenChars = ".\"/\\[]:;=, ";
// By default space-initialize the whole name, append null terminator in the end for both the filename and extension
filename.fill(' ');
extension.fill(' ');
filename[filename.size() - 1] = '\0';
extension[extension.size() - 1] = '\0';
// Find the position of the dot in the string
auto dotPos = path.rfind('.');
// Wikipedia: If a file name has no extension, a trailing . has no effect
// Thus check if the last character is a dot and ignore it, prefering the previous dot if it exists
if (dotPos == path.size() - 1) {
dotPos = path.rfind('.', dotPos); // Get previous dot
}
// If pointPos is not npos we have a valid dot character, and as such an extension
bool haveExtension = dotPos != std::string::npos;
int validCharacterCount = 0;
bool filenameTooBig = false;
// Parse characters until we're done OR until we reach 9 characters, in which case according to Wikipedia we must truncate to 6 letters
// And append ~1 in the end
for (auto c : path.substr(0, dotPos)) {
// Character is forbidden, we must ignore it
if (forbiddenChars.find(c) != std::string::npos) {
continue;
}
// We already have capped the amount of characters, thus our filename is too big
if (validCharacterCount == 8) {
filenameTooBig = true;
break;
}
filename[validCharacterCount++] = convertCharacter(c); // Append character to filename
}
// Truncate name to 6 characters and denote that it is too big
// TODO: Wikipedia says we should also do this if the filename contains an invalid character, including spaces. Must test
if (filenameTooBig) {
filename[6] = '~';
filename[7] = '1';
}
if (haveExtension) {
int extensionLen = 0;
// Copy up to 3 characters from the dot onwards to the extension
for (auto c : path.substr(dotPos + 1, 3)) {
extension[extensionLen++] = convertCharacter(c);
}
}
return {filename, extension};
}
void Kernel::handleDirectoryOperation(u32 messagePointer, Handle directory) {
const u32 cmd = mem.read32(messagePointer);
switch (cmd) {
@ -25,16 +105,77 @@ void Kernel::closeDirectory(u32 messagePointer, Handle directory) {
}
p->getData<DirectorySession>()->isOpen = false;
mem.write32(messagePointer, IPC::responseHeader(0x802, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void Kernel::readDirectory(u32 messagePointer, Handle directory) {
const u32 entryCount = mem.read32(messagePointer + 4);
const u32 outPointer = mem.read32(messagePointer + 12);
logFileIO("Directory::Read (handle = %X, entry count = %d, out pointer = %08X)\n", directory, entryCount, outPointer);
Helpers::panicDev("Unimplemented FsDir::Read");
const auto p = getObject(directory, KernelObjectType::Directory);
if (p == nullptr) [[unlikely]] {
Helpers::panic("Called ReadDirectory on non-existent directory");
}
DirectorySession* session = p->getData<DirectorySession>();
if (!session->pathOnDisk.has_value()) [[unlikely]] {
Helpers::panic("Called ReadDirectory on directory that doesn't have a path on disk");
}
std::filesystem::path dirPath = session->pathOnDisk.value();
int count = 0;
while (count < entryCount && session->currentEntry < session->entries.size()) {
const auto& entry = session->entries[session->currentEntry];
std::filesystem::path path = entry.path;
std::filesystem::path filename = path.filename();
std::filesystem::path relative = path.lexically_relative(dirPath);
bool isDirectory = std::filesystem::is_directory(relative);
std::u16string nameU16 = relative.u16string();
bool isHidden = nameU16[0] == u'.'; // If the first character is a dot then this is a hidden file/folder
const u32 entryPointer = outPointer + (count * 0x228); // 0x228 is the size of a single entry
u32 utfPointer = entryPointer;
u32 namePointer = entryPointer + 0x20C;
u32 extensionPointer = entryPointer + 0x216;
u32 attributePointer = entryPointer + 0x21C;
u32 sizePointer = entryPointer + 0x220;
std::string filenameString = filename.string();
auto [shortFilename, shortExtension] = convertTo83(filenameString);
for (auto c : nameU16) {
mem.write16(utfPointer, u16(c));
utfPointer += sizeof(u16);
}
mem.write16(utfPointer, 0); // Null terminate the UTF16 name
// Write 8.3 filename-extension
for (auto c : shortFilename) {
mem.write8(namePointer, u8(c));
namePointer += sizeof(u8);
}
for (auto c : shortExtension) {
mem.write8(extensionPointer, u8(c));
extensionPointer += sizeof(u8);
}
mem.write8(outPointer + 0x21A, 1); // Always 1 according to 3DBrew
mem.write8(attributePointer, entry.isDirectory ? 1 : 0); // "Is directory" attribute
mem.write8(attributePointer + 1, isHidden ? 1 : 0); // "Is hidden" attribute
mem.write8(attributePointer + 2, entry.isDirectory ? 0 : 1); // "Is archive" attribute
mem.write8(attributePointer + 3, 0); // "Is read-only" attribute
count++; // Increment number of read directories
session->currentEntry++; // Increment index of the entry currently being read
}
mem.write32(messagePointer, IPC::responseHeader(0x801, 2, 2));
mem.write32(messagePointer + 4, Result::Success);
mem.write32(messagePointer + 8, 0);
mem.write32(messagePointer + 8, count);
}

View file

@ -35,22 +35,15 @@ bool Kernel::signalEvent(Handle handle) {
// Check if there's any thread waiting on this event
if (event->waitlist != 0) {
// One-shot events get cleared once they are acquired by some thread and only wake up 1 thread at a time
wakeupAllThreads(event->waitlist, handle);
event->waitlist = 0; // No threads waiting;
if (event->resetType == ResetType::OneShot) {
int index = wakeupOneThread(event->waitlist, handle); // Wake up one thread with the highest priority
event->waitlist ^= (1ull << index); // Remove thread from waitlist
event->fired = false;
} else {
wakeupAllThreads(event->waitlist, handle);
event->waitlist = 0; // No threads waiting;
}
// We must reschedule our threads if we signalled one. Some games such as FE: Awakening rely on this
// If this does not happen, we can have phenomena such as a thread waiting up a higher priority thread,
// and the higher priority thread just never running
rescheduleThreads();
}
rescheduleThreads();
return true;
}
@ -121,7 +114,6 @@ void Kernel::waitSynchronization1() {
if (!shouldWaitOnObject(object)) {
acquireSyncObject(object, threads[currentThreadIndex]); // Acquire the object since it's ready
regs[0] = Result::Success;
rescheduleThreads();
} else {
// Timeout is 0, don't bother waiting, instantly timeout
if (ns == 0) {
@ -141,7 +133,7 @@ void Kernel::waitSynchronization1() {
// Add the current thread to the object's wait list
object->getWaitlist() |= (1ull << currentThreadIndex);
switchToNextThread();
requireReschedule();
}
}
@ -204,14 +196,13 @@ void Kernel::waitSynchronizationN() {
auto& t = threads[currentThreadIndex];
// We only need to wait on one object. Easy...?!
// We only need to wait on one object. Easy.
if (!waitAll) {
// If there's ready objects, acquire the first one and return
if (oneObjectReady) {
regs[0] = Result::Success;
regs[1] = firstReadyObjectIndex; // Return index of the acquired object
acquireSyncObject(waitObjects[firstReadyObjectIndex].second, t); // Acquire object
rescheduleThreads();
return;
}
@ -229,8 +220,8 @@ void Kernel::waitSynchronizationN() {
waitObjects[i].second->getWaitlist() |= (1ull << currentThreadIndex); // And add the thread to the object's waitlist
}
switchToNextThread();
requireReschedule();
} else {
Helpers::panic("WaitSynchronizatioN with waitAll");
Helpers::panic("WaitSynchronizationN with waitAll");
}
}

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;
@ -50,6 +57,7 @@ void Kernel::serviceSVC(u32 svc) {
case 0x25: waitSynchronizationN(); break;
case 0x27: duplicateHandle(); break;
case 0x28: getSystemTick(); break;
case 0x2A: getSystemInfo(); break;
case 0x2B: getProcessInfo(); break;
case 0x2D: connectToPort(); break;
case 0x32: sendSyncRequest(); break;
@ -61,6 +69,8 @@ void Kernel::serviceSVC(u32 svc) {
case 0x3D: outputDebugString(); break;
default: Helpers::panic("Unimplemented svc: %X @ %08X", svc, regs[15]); break;
}
evalReschedule();
}
void Kernel::setVersion(u8 major, u8 minor) {
@ -136,10 +146,13 @@ void Kernel::reset() {
deleteObjectData(object);
}
objects.clear();
mutexHandles.clear();
portHandles.clear();
threadIndices.clear();
serviceManager.reset();
needReschedule = false;
// Allocate handle #0 to a dummy object and make a main process object
makeObject(KernelObjectType::Dummy);
currentProcess = makeProcess(1); // Use ID = 1 for main process
@ -147,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();
@ -249,6 +262,99 @@ 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
PandaInformation = 0x20001,
};
};
namespace CitraInfoType {
enum : u32 {
IsCitra = 0,
BuildName = 10, // (ie: Nightly, Canary).
BuildVersion = 11, // Build version.
BuildDate1 = 20, // Build date first 7 characters.
BuildDate2 = 21, // Build date next 7 characters.
BuildDate3 = 22, // Build date next 7 characters.
BuildDate4 = 23, // Build date last 7 characters.
BuildBranch1 = 30, // Git branch first 7 characters.
BuildBranch2 = 31, // Git branch last 7 characters.
BuildDesc1 = 40, // Git description (commit) first 7 characters.
BuildDesc2 = 41, // Git description (commit) last 7 characters.
};
}
namespace PandaInfoType {
enum : u32 {
IsPanda = 0,
};
}
void Kernel::getSystemInfo() {
const u32 infoType = regs[1];
const u32 subtype = regs[2];
log("GetSystemInfo (type = %X, subtype = %X)\n", infoType, subtype);
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:
// Report that we're not Citra
regs[1] = 0;
regs[2] = 0;
break;
default:
Helpers::warn("GetSystemInfo: Unknown CitraInformation subtype %x\n", subtype);
regs[0] = Result::FailurePlaceholder;
break;
}
break;
}
case SystemInfoType::PandaInformation: {
switch (subtype) {
case PandaInfoType::IsPanda:
// This is indeed us, set output to 1
regs[1] = 1;
regs[2] = 0;
break;
default:
Helpers::warn("GetSystemInfo: Unknown PandaInformation subtype %x\n", subtype);
regs[0] = Result::FailurePlaceholder;
break;
}
break;
}
default: Helpers::panic("GetSystemInfo: Unknown system info type: %x (subtype: %x)\n", infoType, subtype); break;
}
}
std::string Kernel::getProcessName(u32 pid) {
if (pid == KernelHandles::CurrentProcess) {
return "current";

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

@ -76,6 +76,11 @@ void Kernel::sendSyncRequest() {
u32 messagePointer = getTLSPointer() + 0x80; // The message is stored starting at TLS+0x80
logSVC("SendSyncRequest(session handle = %X)\n", handle);
// Service calls via SendSyncRequest and file access needs to put the caller to sleep for a given amount of time
// To make sure that the other threads don't get starved. Various games rely on this (including Sonic Boom: Shattering Crystal it seems)
constexpr u64 syncRequestDelayNs = 39000;
sleepThread(syncRequestDelayNs);
// The sync request is being sent at a service rather than whatever port, so have the service manager intercept it
if (KernelHandles::isServiceHandle(handle)) {
// The service call might cause a reschedule and change threads. Hence, set r0 before executing the service call
@ -104,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;
}
@ -122,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

@ -82,37 +82,31 @@ std::optional<int> Kernel::getNextThread() {
return std::nullopt;
}
void Kernel::switchToNextThread() {
std::optional<int> newThreadIndex = getNextThread();
if (!newThreadIndex.has_value()) {
log("Kernel tried to switch to the next thread but none found. Switching to random thread\n");
assert(aliveThreadCount != 0);
Helpers::panic("rpog");
int index;
do {
index = rand() % threadCount;
} while (threads[index].status == ThreadStatus::Dead); // TODO: Pray this doesn't hang
switchThread(index);
} else {
switchThread(newThreadIndex.value());
}
}
// See if there;s a higher priority, ready thread and switch to that
// See if there is a higher priority, ready thread and switch to that
void Kernel::rescheduleThreads() {
Thread& current = threads[currentThreadIndex]; // Current running thread
// If the current thread is running and hasn't gone to sleep or whatever, set it to Ready instead of Running
// So that getNextThread will evaluate it properly
if (current.status == ThreadStatus::Running) {
current.status = ThreadStatus::Ready;
}
ThreadStatus currentStatus = current.status;
std::optional<int> newThreadIndex = getNextThread();
if (newThreadIndex.has_value() && newThreadIndex.value() != currentThreadIndex) {
threads[currentThreadIndex].status = ThreadStatus::Ready;
// Case 1: A thread can run
if (newThreadIndex.has_value()) {
switchThread(newThreadIndex.value());
}
// Case 2: No other thread can run, straight to the idle thread
else {
switchThread(idleThreadIndex);
}
}
// 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
@ -174,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;
}
@ -184,6 +183,7 @@ void Kernel::releaseMutex(Mutex* moo) {
// If the lock count reached 0 then the thread no longer owns the mootex and it can be given to a new one
if (moo->lockCount == 0) {
moo->locked = false;
if (moo->waitlist != 0) {
int index = wakeupOneThread(moo->waitlist, moo->handle); // Wake up one thread and get its index
moo->waitlist ^= (1ull << index); // Remove thread from waitlist
@ -194,7 +194,7 @@ void Kernel::releaseMutex(Mutex* moo) {
moo->ownerThread = index;
}
rescheduleThreads();
requireReschedule();
}
}
@ -210,7 +210,7 @@ void Kernel::sleepThreadOnArbiter(u32 waitingAddress) {
t.status = ThreadStatus::WaitArbiter;
t.waitingAddress = waitingAddress;
switchToNextThread();
requireReschedule();
}
// Acquires an object that is **ready to be acquired** without waiting on it
@ -226,7 +226,13 @@ void Kernel::acquireSyncObject(KernelObject* object, const Thread& thread) {
case KernelObjectType::Mutex: {
Mutex* moo = object->getData<Mutex>();
moo->locked = true; // Set locked to true, whether it's false or not because who cares
// Only reschedule if we're acquiring the mutex for the first time
if (!moo->locked) {
moo->locked = true;
requireReschedule();
}
// Increment lock count by 1. If a thread acquires a mootex multiple times, it needs to release it until count == 0
// For the mootex to be free.
moo->lockCount++;
@ -338,20 +344,31 @@ void Kernel::wakeupAllThreads(u64 waitlist, Handle handle) {
void Kernel::sleepThread(s64 ns) {
if (ns < 0) {
Helpers::panic("Sleeping a thread for a negative amount of ns");
} else if (ns == 0) { // Used when we want to force a thread switch
std::optional<int> newThreadIndex = getNextThread();
// If there's no other thread waiting, don't bother yielding
if (newThreadIndex.has_value()) {
threads[currentThreadIndex].status = ThreadStatus::Ready;
switchThread(newThreadIndex.value());
}
} else { // If we're sleeping for > 0 ns
} else if (ns == 0) {
// TODO: This is garbage, but it works so eh we can keep it for now
Thread& t = threads[currentThreadIndex];
// See if a thread other than this and the idle thread is waiting to run by temp marking the current function as dead and searching
// If there is another thread to run, then run it. Otherwise, go back to this thread, not to the idle thread
t.status = ThreadStatus::Dead;
auto nextThreadIndex = getNextThread();
t.status = ThreadStatus::Ready;
if (nextThreadIndex.has_value()) {
const auto index = nextThreadIndex.value();
if (index != idleThreadIndex) {
switchThread(index);
}
}
} else { // If we're sleeping for >= 0 ns
Thread& t = threads[currentThreadIndex];
t.status = ThreadStatus::WaitSleep;
t.waitingNanoseconds = ns;
t.sleepTick = cpu.getTicks();
switchToNextThread();
requireReschedule();
}
}
@ -372,9 +389,13 @@ 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);
rescheduleThreads();
regs[1] = makeThread(entrypoint, initialSP, priority, static_cast<ProcessorID>(id), arg, ThreadStatus::Ready);
requireReschedule();
}
// void SleepThread(s64 nanoseconds)
@ -424,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];
@ -448,12 +478,56 @@ void Kernel::setThreadPriority() {
}
}
sortThreads();
rescheduleThreads();
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)
@ -472,7 +546,7 @@ void Kernel::exitThread() {
t.threadsWaitingForTermination = 0; // No other threads waiting
}
switchToNextThread();
requireReschedule();
}
void Kernel::svcCreateMutex() {
@ -585,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;
@ -159,8 +166,9 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn
// Read ExeFS
if (hasExeFS()) {
u64 exeFSOffset = fileOffset + exeFS.offset; // Offset of ExeFS in the file = exeFS offset + ncch offset
printf("ExeFS offset: %08llX, size: %08llX (Offset in file = %08llX)\n", exeFS.offset, exeFS.size, exeFSOffset);
// Offset of ExeFS in the file = exeFS offset + NCCH offset
// exeFS.offset has already been offset by the NCCH offset
printf("ExeFS offset: %08llX, size: %08llX (Offset in file = %08llX)\n", exeFS.offset - info.offset, exeFS.size, exeFS.offset);
constexpr size_t exeFSHeaderSize = 0x200;
u8 exeFSHeader[exeFSHeaderSize];
@ -208,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);
}
@ -220,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);
}
}
@ -135,10 +165,22 @@ u32 Memory::read32(u32 vaddr) {
case ConfigMem::SyscoreVer: return 2;
case 0x1FF81000: return 0; // TODO: Figure out what this config mem address does
case ConfigMem::WifiMac: return 0xFF07F440; // Wifi MAC: First 4 bytes of MAC Address
// 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);
@ -390,7 +432,8 @@ MemoryInfo Memory::queryMemory(u32 vaddr) {
u8* Memory::mapSharedMemory(Handle handle, u32 vaddr, u32 myPerms, u32 otherPerms) {
for (auto& e : sharedMemBlocks) {
if (e.handle == handle) {
if (e.mapped) Helpers::panic("Allocated shared memory block twice. Is this allowed?");
// Virtual Console titles trigger this. TODO: Investigate how it should work
if (e.mapped) Helpers::warn("Allocated shared memory block twice. Is this allowed?");
const u32 paddr = e.paddr;
const u32 size = e.size;
@ -459,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

@ -9,7 +9,7 @@ static constexpr u32 signExtend3To32(u32 val) {
return (u32)(s32(val) << 29 >> 29);
}
u32 Texture::getTexelETC(bool hasAlpha, u32 u, u32 v, u32 width, const void* data) {
u32 Texture::getTexelETC(bool hasAlpha, u32 u, u32 v, u32 width, std::span<const u8> data) {
// Pixel offset of the 8x8 tile based on u, v and the width of the texture
u32 offs = ((u & ~7) * 8) + ((v & ~7) * width);
if (!hasAlpha)
@ -30,8 +30,7 @@ u32 Texture::getTexelETC(bool hasAlpha, u32 u, u32 v, u32 width, const void* dat
offs += subTileSize * subTileIndex;
u32 alpha;
const u8* tmp = static_cast<const u8*>(data) + offs; // Pointer to colour and alpha data as u8*
const u64* ptr = reinterpret_cast<const u64*>(tmp); // Cast to u64*
const u64* ptr = reinterpret_cast<const u64*>(data.data() + offs); // Cast to u64*
if (hasAlpha) {
// First 64 bits of the 4x4 subtile are alpha data
@ -118,4 +117,4 @@ u32 Texture::decodeETC(u32 alpha, u32 u, u32 v, u64 colourData) {
b = std::clamp(b + modifier, 0, 255);
return (alpha << 24) | (u32(b) << 16) | (u32(g) << 8) | u32(r);
}
}

View file

@ -2,7 +2,29 @@
void GLStateManager::resetBlend() {
blendEnabled = false;
logicOpEnabled = false;
logicOp = GL_COPY;
OpenGL::disableBlend();
OpenGL::disableLogicOp();
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;
for (int i = 0; i < clipPlaneCount; i++) {
OpenGL::disableClipPlane(i);
}
}
void GLStateManager::resetColourMask() {
@ -26,6 +48,14 @@ void GLStateManager::resetScissor() {
OpenGL::setScissor(0, 0, 0, 0);
}
void GLStateManager::resetStencil() {
stencilEnabled = false;
stencilMask = 0xff;
OpenGL::disableStencil();
OpenGL::setStencilMask(0xff);
}
void GLStateManager::resetVAO() {
boundVAO = 0;
glBindVertexArray(0);
@ -43,6 +73,8 @@ void GLStateManager::resetProgram() {
void GLStateManager::reset() {
resetBlend();
resetClearing();
resetClipping();
resetColourMask();
resetDepth();
@ -50,4 +82,5 @@ void GLStateManager::reset() {
resetVBO();
resetProgram();
resetScissor();
resetStencil();
}

File diff suppressed because it is too large Load diff

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);
}
@ -112,12 +117,11 @@ u32 Texture::getSwizzledOffset_4bpp(u32 u, u32 v, u32 width) {
// Get the texel at position (u, v)
// fmt: format of the texture
// data: texture data of the texture
u32 Texture::decodeTexel(u32 u, u32 v, PICA::TextureFmt fmt, const void* data) {
u32 Texture::decodeTexel(u32 u, u32 v, PICA::TextureFmt fmt, std::span<const u8> data) {
switch (fmt) {
case PICA::TextureFmt::RGBA4: {
u32 offset = getSwizzledOffset(u, v, size.u(), 2);
auto ptr = static_cast<const u8*>(data);
u16 texel = u16(ptr[offset]) | (u16(ptr[offset + 1]) << 8);
u16 texel = u16(data[offset]) | (u16(data[offset + 1]) << 8);
u8 alpha = Colour::convert4To8Bit(getBits<0, 4, u8>(texel));
u8 b = Colour::convert4To8Bit(getBits<4, 4, u8>(texel));
@ -128,9 +132,8 @@ u32 Texture::decodeTexel(u32 u, u32 v, PICA::TextureFmt fmt, const void* data) {
}
case PICA::TextureFmt::RGBA5551: {
u32 offset = getSwizzledOffset(u, v, size.u(), 2);
auto ptr = static_cast<const u8*>(data);
u16 texel = u16(ptr[offset]) | (u16(ptr[offset + 1]) << 8);
const u32 offset = getSwizzledOffset(u, v, size.u(), 2);
const u16 texel = u16(data[offset]) | (u16(data[offset + 1]) << 8);
u8 alpha = getBit<0>(texel) ? 0xff : 0;
u8 b = Colour::convert5To8Bit(getBits<1, 5, u8>(texel));
@ -141,56 +144,47 @@ u32 Texture::decodeTexel(u32 u, u32 v, PICA::TextureFmt fmt, const void* data) {
}
case PICA::TextureFmt::RGB565: {
u32 offset = getSwizzledOffset(u, v, size.u(), 2);
auto ptr = static_cast<const u8*>(data);
u16 texel = u16(ptr[offset]) | (u16(ptr[offset + 1]) << 8);
const u32 offset = getSwizzledOffset(u, v, size.u(), 2);
const u16 texel = u16(data[offset]) | (u16(data[offset + 1]) << 8);
u8 b = Colour::convert5To8Bit(getBits<0, 5, u8>(texel));
u8 g = Colour::convert6To8Bit(getBits<5, 6, u8>(texel));
u8 r = Colour::convert5To8Bit(getBits<11, 5, u8>(texel));
const u8 b = Colour::convert5To8Bit(getBits<0, 5, u8>(texel));
const u8 g = Colour::convert6To8Bit(getBits<5, 6, u8>(texel));
const u8 r = Colour::convert5To8Bit(getBits<11, 5, u8>(texel));
return (0xff << 24) | (b << 16) | (g << 8) | r;
}
case PICA::TextureFmt::RG8: {
u32 offset = getSwizzledOffset(u, v, size.u(), 2);
auto ptr = static_cast<const u8*>(data);
constexpr u8 b = 0;
u8 g = ptr[offset];
u8 r = ptr[offset + 1];
const u8 g = data[offset];
const u8 r = data[offset + 1];
return (0xff << 24) | (b << 16) | (g << 8) | r;
}
case PICA::TextureFmt::RGB8: {
u32 offset = getSwizzledOffset(u, v, size.u(), 3);
auto ptr = static_cast<const u8*>(data);
u8 b = ptr[offset];
u8 g = ptr[offset + 1];
u8 r = ptr[offset + 2];
const u32 offset = getSwizzledOffset(u, v, size.u(), 3);
const u8 b = data[offset];
const u8 g = data[offset + 1];
const u8 r = data[offset + 2];
return (0xff << 24) | (b << 16) | (g << 8) | r;
}
case PICA::TextureFmt::RGBA8: {
u32 offset = getSwizzledOffset(u, v, size.u(), 4);
auto ptr = static_cast<const u8*>(data);
u8 alpha = ptr[offset];
u8 b = ptr[offset + 1];
u8 g = ptr[offset + 2];
u8 r = ptr[offset + 3];
const u32 offset = getSwizzledOffset(u, v, size.u(), 4);
const u8 alpha = data[offset];
const u8 b = data[offset + 1];
const u8 g = data[offset + 2];
const u8 r = data[offset + 3];
return (alpha << 24) | (b << 16) | (g << 8) | r;
}
case PICA::TextureFmt::IA4: {
u32 offset = getSwizzledOffset(u, v, size.u(), 1);
auto ptr = static_cast<const u8*>(data);
const u8 texel = ptr[offset];
const u32 offset = getSwizzledOffset(u, v, size.u(), 1);
const u8 texel = data[offset];
const u8 alpha = Colour::convert4To8Bit(texel & 0xf);
const u8 intensity = Colour::convert4To8Bit(texel >> 4);
@ -199,11 +193,10 @@ u32 Texture::decodeTexel(u32 u, u32 v, PICA::TextureFmt fmt, const void* data) {
}
case PICA::TextureFmt::A4: {
u32 offset = getSwizzledOffset_4bpp(u, v, size.u());
auto ptr = static_cast<const u8*>(data);
const u32 offset = getSwizzledOffset_4bpp(u, v, size.u());
// For odd U coordinates, grab the top 4 bits, and the low 4 bits for even coordinates
u8 alpha = ptr[offset] >> ((u % 2) ? 4 : 0);
u8 alpha = data[offset] >> ((u % 2) ? 4 : 0);
alpha = Colour::convert4To8Bit(getBits<0, 4>(alpha));
// A8 sets RGB to 0
@ -212,8 +205,7 @@ u32 Texture::decodeTexel(u32 u, u32 v, PICA::TextureFmt fmt, const void* data) {
case PICA::TextureFmt::A8: {
u32 offset = getSwizzledOffset(u, v, size.u(), 1);
auto ptr = static_cast<const u8*>(data);
const u8 alpha = ptr[offset];
const u8 alpha = data[offset];
// A8 sets RGB to 0
return (alpha << 24) | (0 << 16) | (0 << 8) | 0;
@ -221,10 +213,9 @@ u32 Texture::decodeTexel(u32 u, u32 v, PICA::TextureFmt fmt, const void* data) {
case PICA::TextureFmt::I4: {
u32 offset = getSwizzledOffset_4bpp(u, v, size.u());
auto ptr = static_cast<const u8*>(data);
// For odd U coordinates, grab the top 4 bits, and the low 4 bits for even coordinates
u8 intensity = ptr[offset] >> ((u % 2) ? 4 : 0);
u8 intensity = data[offset] >> ((u % 2) ? 4 : 0);
intensity = Colour::convert4To8Bit(getBits<0, 4>(intensity));
// Intensity formats just copy the intensity value to every colour channel
@ -233,8 +224,7 @@ u32 Texture::decodeTexel(u32 u, u32 v, PICA::TextureFmt fmt, const void* data) {
case PICA::TextureFmt::I8: {
u32 offset = getSwizzledOffset(u, v, size.u(), 1);
auto ptr = static_cast<const u8*>(data);
const u8 intensity = ptr[offset];
const u8 intensity = data[offset];
// Intensity formats just copy the intensity value to every colour channel
return (0xff << 24) | (intensity << 16) | (intensity << 8) | intensity;
@ -242,11 +232,10 @@ u32 Texture::decodeTexel(u32 u, u32 v, PICA::TextureFmt fmt, const void* data) {
case PICA::TextureFmt::IA8: {
u32 offset = getSwizzledOffset(u, v, size.u(), 2);
auto ptr = static_cast<const u8*>(data);
// Same as I8 except each pixel gets its own alpha value too
const u8 alpha = ptr[offset];
const u8 intensity = ptr[offset + 1];
const u8 alpha = data[offset];
const u8 intensity = data[offset + 1];
return (alpha << 24) | (intensity << 16) | (intensity << 8) | intensity;
}
@ -258,7 +247,7 @@ u32 Texture::decodeTexel(u32 u, u32 v, PICA::TextureFmt fmt, const void* data) {
}
}
void Texture::decodeTexture(const void* data) {
void Texture::decodeTexture(std::span<const u8> data) {
std::vector<u32> decoded;
decoded.reserve(u64(size.u()) * u64(size.v()));
@ -272,4 +261,4 @@ void Texture::decodeTexture(const void* data) {
texture.bind();
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, size.u(), size.v(), GL_RGBA, GL_UNSIGNED_BYTE, decoded.data());
}
}

View file

@ -0,0 +1,14 @@
#include "renderer_null/renderer_null.hpp"
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() {}
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) {}

View file

@ -0,0 +1,25 @@
#include "renderer_sw/renderer_sw.hpp"
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"); }
void RendererSw::display() { printf("RendererSW: Unimplemented display call\n"); }
void RendererSw::initGraphicsContext(SDL_Window* window) { printf("RendererSW: Unimplemented initGraphicsContext call\n"); }
void RendererSw::clearBuffer(u32 startAddress, u32 endAddress, u32 value, u32 control) { printf("RendererSW: Unimplemented clearBuffer call\n"); }
void RendererSw::displayTransfer(u32 inputAddr, u32 outputAddr, u32 inputSize, u32 outputSize, u32 flags) {
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"); }

View file

@ -0,0 +1,549 @@
#include "renderer_vk/renderer_vk.hpp"
#include <limits>
#include <span>
#include <unordered_set>
#include "SDL_vulkan.h"
#include "helpers.hpp"
#include "renderer_vk/vk_debug.hpp"
// Finds the first queue family that satisfies `queueMask` and excludes `queueExcludeMask` bits
// Returns -1 if not found
// Todo: Smarter selection for present/graphics/compute/transfer
static s32 findQueueFamily(
std::span<const vk::QueueFamilyProperties> queueFamilies, vk::QueueFlags queueMask,
vk::QueueFlags queueExcludeMask = vk::QueueFlagBits::eProtected
) {
for (usize i = 0; i < queueFamilies.size(); ++i) {
if (((queueFamilies[i].queueFlags & queueMask) == queueMask) && !(queueFamilies[i].queueFlags & queueExcludeMask)) {
return i;
}
}
return -1;
}
vk::Result RendererVK::recreateSwapchain(vk::SurfaceKHR surface, vk::Extent2D swapchainExtent) {
static constexpr u32 screenTextureWidth = 400; // Top screen is 400 pixels wide, bottom is 320
static constexpr u32 screenTextureHeight = 2 * 240; // Both screens are 240 pixels tall
static constexpr vk::ImageUsageFlags swapchainUsageFlagsRequired =
(vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst);
// Extent + Image count + Usage + Surface Transform
vk::ImageUsageFlags swapchainImageUsage;
vk::SurfaceTransformFlagBitsKHR swapchainSurfaceTransform;
if (const auto getResult = physicalDevice.getSurfaceCapabilitiesKHR(surface); getResult.result == vk::Result::eSuccess) {
const vk::SurfaceCapabilitiesKHR& surfaceCapabilities = getResult.value;
// In the case if width == height == -1, we define the extent ourselves but must fit within the limits
if (surfaceCapabilities.currentExtent.width == -1 || surfaceCapabilities.currentExtent.height == -1) {
swapchainExtent.width = std::max(swapchainExtent.width, surfaceCapabilities.minImageExtent.width);
swapchainExtent.height = std::max(swapchainExtent.height, surfaceCapabilities.minImageExtent.height);
swapchainExtent.width = std::min(swapchainExtent.width, surfaceCapabilities.maxImageExtent.width);
swapchainExtent.height = std::min(swapchainExtent.height, surfaceCapabilities.maxImageExtent.height);
}
swapchainImageCount = surfaceCapabilities.minImageCount + 1;
if ((surfaceCapabilities.maxImageCount > 0) && (swapchainImageCount > surfaceCapabilities.maxImageCount)) {
swapchainImageCount = surfaceCapabilities.maxImageCount;
}
swapchainImageUsage = surfaceCapabilities.supportedUsageFlags & swapchainUsageFlagsRequired;
if ((swapchainImageUsage & swapchainUsageFlagsRequired) != swapchainUsageFlagsRequired) {
Helpers::panic(
"Unsupported swapchain image usage. Could not acquire %s\n", vk::to_string(swapchainImageUsage ^ swapchainUsageFlagsRequired).c_str()
);
}
if (surfaceCapabilities.supportedTransforms & vk::SurfaceTransformFlagBitsKHR::eIdentity) {
swapchainSurfaceTransform = vk::SurfaceTransformFlagBitsKHR::eIdentity;
} else {
swapchainSurfaceTransform = surfaceCapabilities.currentTransform;
}
} else {
Helpers::panic("Error getting surface capabilities: %s\n", vk::to_string(getResult.result).c_str());
}
// Preset Mode
// Fifo support is required by all vulkan implementations, waits for vsync
vk::PresentModeKHR swapchainPresentMode = vk::PresentModeKHR::eFifo;
if (auto getResult = physicalDevice.getSurfacePresentModesKHR(surface); getResult.result == vk::Result::eSuccess) {
std::vector<vk::PresentModeKHR>& presentModes = getResult.value;
// Use mailbox if available, lowest-latency vsync-enabled mode
if (std::find(presentModes.begin(), presentModes.end(), vk::PresentModeKHR::eMailbox) != presentModes.end()) {
swapchainPresentMode = vk::PresentModeKHR::eMailbox;
}
} else {
Helpers::panic("Error enumerating surface present modes: %s\n", vk::to_string(getResult.result).c_str());
}
// Surface format
vk::SurfaceFormatKHR swapchainSurfaceFormat;
if (auto getResult = physicalDevice.getSurfaceFormatsKHR(surface); getResult.result == vk::Result::eSuccess) {
std::vector<vk::SurfaceFormatKHR>& surfaceFormats = getResult.value;
// A singular undefined surface format means we can use any format we want
if ((surfaceFormats.size() == 1) && surfaceFormats[0].format == vk::Format::eUndefined) {
// Assume R8G8B8A8-SRGB by default
swapchainSurfaceFormat = {vk::Format::eR8G8B8A8Unorm, vk::ColorSpaceKHR::eSrgbNonlinear};
} else {
// Find the next-best R8G8B8A8-SRGB format
std::vector<vk::SurfaceFormatKHR>::iterator partitionEnd = surfaceFormats.end();
const auto preferR8G8B8A8 = [](const vk::SurfaceFormatKHR& surfaceFormat) -> bool {
return surfaceFormat.format == vk::Format::eR8G8B8A8Snorm;
};
partitionEnd = std::stable_partition(surfaceFormats.begin(), partitionEnd, preferR8G8B8A8);
const auto preferSrgbNonLinear = [](const vk::SurfaceFormatKHR& surfaceFormat) -> bool {
return surfaceFormat.colorSpace == vk::ColorSpaceKHR::eSrgbNonlinear;
};
partitionEnd = std::stable_partition(surfaceFormats.begin(), partitionEnd, preferSrgbNonLinear);
swapchainSurfaceFormat = surfaceFormats.front();
}
} else {
Helpers::panic("Error enumerating surface formats: %s\n", vk::to_string(getResult.result).c_str());
}
vk::SwapchainCreateInfoKHR swapchainInfo = {};
swapchainInfo.surface = surface;
swapchainInfo.minImageCount = swapchainImageCount;
swapchainInfo.imageFormat = swapchainSurfaceFormat.format;
swapchainInfo.imageColorSpace = swapchainSurfaceFormat.colorSpace;
swapchainInfo.imageExtent = swapchainExtent;
swapchainInfo.imageArrayLayers = 1;
swapchainInfo.imageUsage = swapchainImageUsage;
swapchainInfo.imageSharingMode = vk::SharingMode::eExclusive;
swapchainInfo.preTransform = swapchainSurfaceTransform;
swapchainInfo.compositeAlpha = vk::CompositeAlphaFlagBitsKHR::eOpaque;
swapchainInfo.presentMode = swapchainPresentMode;
swapchainInfo.clipped = true;
swapchainInfo.oldSwapchain = swapchain.get();
if (auto createResult = device->createSwapchainKHRUnique(swapchainInfo); createResult.result == vk::Result::eSuccess) {
swapchain = std::move(createResult.value);
} else {
Helpers::panic("Error creating swapchain: %s\n", vk::to_string(createResult.result).c_str());
}
// Get swapchain images
if (auto getResult = device->getSwapchainImagesKHR(swapchain.get()); getResult.result == vk::Result::eSuccess) {
swapchainImages = getResult.value;
swapchainImageViews.resize(swapchainImages.size());
// Create image-views
for (usize i = 0; i < swapchainImages.size(); i++) {
vk::ImageViewCreateInfo viewInfo = {};
viewInfo.image = swapchainImages[i];
viewInfo.viewType = vk::ImageViewType::e2D;
viewInfo.format = swapchainSurfaceFormat.format;
viewInfo.components = vk::ComponentMapping();
viewInfo.subresourceRange = vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1);
if (auto createResult = device->createImageViewUnique(viewInfo); createResult.result == vk::Result::eSuccess) {
swapchainImageViews[i] = std::move(createResult.value);
} else {
Helpers::panic("Error creating swapchain image-view: #%zu %s\n", i, vk::to_string(getResult.result).c_str());
}
}
} else {
Helpers::panic("Error creating acquiring swapchain images: %s\n", vk::to_string(getResult.result).c_str());
}
// Swapchain Command buffer(s)
vk::CommandBufferAllocateInfo commandBuffersInfo = {};
commandBuffersInfo.commandPool = commandPool.get();
commandBuffersInfo.level = vk::CommandBufferLevel::ePrimary;
commandBuffersInfo.commandBufferCount = swapchainImageCount;
if (auto allocateResult = device->allocateCommandBuffersUnique(commandBuffersInfo); allocateResult.result == vk::Result::eSuccess) {
presentCommandBuffers = std::move(allocateResult.value);
} else {
Helpers::panic("Error allocating command buffer: %s\n", vk::to_string(allocateResult.result).c_str());
}
// Swapchain synchronization primitives
vk::FenceCreateInfo fenceInfo = {};
fenceInfo.flags = vk::FenceCreateFlagBits::eSignaled;
vk::SemaphoreCreateInfo semaphoreInfo = {};
swapImageFreeSemaphore.resize(swapchainImageCount);
renderFinishedSemaphore.resize(swapchainImageCount);
frameFinishedFences.resize(swapchainImageCount);
for (usize i = 0; i < swapchainImageCount; i++) {
if (auto createResult = device->createSemaphoreUnique(semaphoreInfo); createResult.result == vk::Result::eSuccess) {
swapImageFreeSemaphore[i] = std::move(createResult.value);
} else {
Helpers::panic("Error creating 'present-ready' semaphore: %s\n", vk::to_string(createResult.result).c_str());
}
if (auto createResult = device->createSemaphoreUnique(semaphoreInfo); createResult.result == vk::Result::eSuccess) {
renderFinishedSemaphore[i] = std::move(createResult.value);
} else {
Helpers::panic("Error creating 'post-render' semaphore: %s\n", vk::to_string(createResult.result).c_str());
}
if (auto createResult = device->createFenceUnique(fenceInfo); createResult.result == vk::Result::eSuccess) {
frameFinishedFences[i] = std::move(createResult.value);
} else {
Helpers::panic("Error creating 'present-ready' semaphore: %s\n", vk::to_string(createResult.result).c_str());
}
}
return vk::Result::eSuccess;
}
RendererVK::RendererVK(GPU& gpu, const std::array<u32, regNum>& internalRegs, const std::array<u32, extRegNum>& externalRegs)
: Renderer(gpu, internalRegs, externalRegs) {}
RendererVK::~RendererVK() {}
void RendererVK::reset() {}
void RendererVK::display() {
// Block, on the CPU, to ensure that this swapchain-frame is ready for more work
if (auto waitResult = device->waitForFences({frameFinishedFences[currentFrame].get()}, true, std::numeric_limits<u64>::max());
waitResult != vk::Result::eSuccess) {
Helpers::panic("Error waiting on swapchain fence: %s\n", vk::to_string(waitResult).c_str());
}
u32 swapchainImageIndex = std::numeric_limits<u32>::max();
if (const auto acquireResult =
device->acquireNextImageKHR(swapchain.get(), std::numeric_limits<u64>::max(), swapImageFreeSemaphore[currentFrame].get(), {});
acquireResult.result == vk::Result::eSuccess) {
swapchainImageIndex = acquireResult.value;
} else {
switch (acquireResult.result) {
case vk::Result::eSuboptimalKHR:
case vk::Result::eErrorOutOfDateKHR: {
// Surface resized
vk::Extent2D swapchainExtent;
{
int windowWidth, windowHeight;
// Block until we have a valid surface-area to present to
// Usually this is because the window has been minimized
// Todo: We should still be rendering even without a valid swapchain
do {
SDL_Vulkan_GetDrawableSize(targetWindow, &windowWidth, &windowHeight);
} while (!windowWidth || !windowHeight);
swapchainExtent.width = windowWidth;
swapchainExtent.height = windowHeight;
}
recreateSwapchain(surface.get(), swapchainExtent);
break;
}
default: {
Helpers::panic("Error acquiring next swapchain image: %s\n", vk::to_string(acquireResult.result).c_str());
}
}
}
vk::UniqueCommandBuffer& presentCommandBuffer = presentCommandBuffers.at(currentFrame);
vk::CommandBufferBeginInfo beginInfo = {};
beginInfo.flags = vk::CommandBufferUsageFlagBits::eSimultaneousUse;
if (const vk::Result beginResult = presentCommandBuffer->begin(beginInfo); beginResult != vk::Result::eSuccess) {
Helpers::panic("Error beginning command buffer recording: %s\n", vk::to_string(beginResult).c_str());
}
{
static const std::array<float, 4> presentScopeColor = {{1.0f, 0.0f, 1.0f, 1.0f}};
Vulkan::DebugLabelScope debugScope(presentCommandBuffer.get(), presentScopeColor, "Present");
// Prepare for color-clear
presentCommandBuffer->pipelineBarrier(
vk::PipelineStageFlagBits::eAllCommands, vk::PipelineStageFlagBits::eTransfer, vk::DependencyFlags(), {}, {},
{vk::ImageMemoryBarrier(
vk::AccessFlagBits::eMemoryRead, vk::AccessFlagBits::eTransferWrite, vk::ImageLayout::eUndefined,
vk::ImageLayout::eTransferDstOptimal, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, swapchainImages[swapchainImageIndex],
vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1)
)}
);
presentCommandBuffer->clearColorImage(
swapchainImages[swapchainImageIndex], vk::ImageLayout::eTransferDstOptimal, presentScopeColor,
vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1)
);
// Prepare for present
presentCommandBuffer->pipelineBarrier(
vk::PipelineStageFlagBits::eTransfer, vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::DependencyFlags(), {}, {},
{vk::ImageMemoryBarrier(
vk::AccessFlagBits::eNone, vk::AccessFlagBits::eColorAttachmentWrite, vk::ImageLayout::eTransferDstOptimal,
vk::ImageLayout::ePresentSrcKHR, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, swapchainImages[swapchainImageIndex],
vk::ImageSubresourceRange(vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1)
)}
);
}
if (const vk::Result endResult = presentCommandBuffer->end(); endResult != vk::Result::eSuccess) {
Helpers::panic("Error ending command buffer recording: %s\n", vk::to_string(endResult).c_str());
}
vk::SubmitInfo submitInfo = {};
// Wait for any previous uses of the image image to finish presenting
submitInfo.setWaitSemaphores(swapImageFreeSemaphore[currentFrame].get());
// Signal when finished
submitInfo.setSignalSemaphores(renderFinishedSemaphore[currentFrame].get());
static const vk::PipelineStageFlags waitStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput;
submitInfo.setWaitDstStageMask(waitStageMask);
submitInfo.setCommandBuffers(presentCommandBuffer.get());
device->resetFences({frameFinishedFences[currentFrame].get()});
if (const vk::Result submitResult = graphicsQueue.submit({submitInfo}, frameFinishedFences[currentFrame].get());
submitResult != vk::Result::eSuccess) {
Helpers::panic("Error submitting to graphics queue: %s\n", vk::to_string(submitResult).c_str());
}
vk::PresentInfoKHR presentInfo = {};
presentInfo.setWaitSemaphores(renderFinishedSemaphore[currentFrame].get());
presentInfo.setSwapchains(swapchain.get());
presentInfo.setImageIndices(swapchainImageIndex);
if (const auto presentResult = presentQueue.presentKHR(presentInfo); presentResult == vk::Result::eSuccess) {
} else {
switch (presentResult) {
case vk::Result::eSuboptimalKHR:
case vk::Result::eErrorOutOfDateKHR: {
// Surface resized
vk::Extent2D swapchainExtent;
{
int windowWidth, windowHeight;
SDL_Vulkan_GetDrawableSize(targetWindow, &windowWidth, &windowHeight);
swapchainExtent.width = windowWidth;
swapchainExtent.height = windowHeight;
}
recreateSwapchain(surface.get(), swapchainExtent);
break;
}
default: {
Helpers::panic("Error presenting swapchain image: %s\n", vk::to_string(presentResult).c_str());
}
}
}
currentFrame = ((currentFrame + 1) % swapchainImageCount);
}
void RendererVK::initGraphicsContext(SDL_Window* window) {
targetWindow = window;
// Resolve all instance function pointers
static vk::DynamicLoader dl;
VULKAN_HPP_DEFAULT_DISPATCHER.init(dl.getProcAddress<PFN_vkGetInstanceProcAddr>("vkGetInstanceProcAddr"));
// Create Instance
vk::ApplicationInfo applicationInfo = {};
applicationInfo.apiVersion = VK_API_VERSION_1_1;
applicationInfo.pEngineName = "Alber";
applicationInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
applicationInfo.pApplicationName = "Alber";
applicationInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
vk::InstanceCreateInfo instanceInfo = {};
instanceInfo.pApplicationInfo = &applicationInfo;
std::vector<const char*> instanceExtensions = {
#if defined(__APPLE__)
VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME,
#endif
VK_EXT_DEBUG_UTILS_EXTENSION_NAME,
};
// Get any additional extensions that SDL wants as well
{
unsigned int extensionCount = 0;
SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, nullptr);
std::vector<const char*> sdlInstanceExtensions(extensionCount);
SDL_Vulkan_GetInstanceExtensions(window, &extensionCount, sdlInstanceExtensions.data());
instanceExtensions.insert(instanceExtensions.end(), sdlInstanceExtensions.begin(), sdlInstanceExtensions.end());
}
#if defined(__APPLE__)
instanceInfo.flags |= vk::InstanceCreateFlagBits::eEnumeratePortabilityKHR;
#endif
instanceInfo.ppEnabledExtensionNames = instanceExtensions.data();
instanceInfo.enabledExtensionCount = instanceExtensions.size();
if (auto createResult = vk::createInstanceUnique(instanceInfo); createResult.result == vk::Result::eSuccess) {
instance = std::move(createResult.value);
} else {
Helpers::panic("Error creating Vulkan instance: %s\n", vk::to_string(createResult.result).c_str());
}
// Initialize instance-specific function pointers
VULKAN_HPP_DEFAULT_DISPATCHER.init(instance.get());
// Enable debug messenger if the instance was able to be created with debug_utils
if (std::find(
instanceExtensions.begin(), instanceExtensions.end(),
// std::string_view has a way to compare itself to `const char*`
// so by casting it, we get the actual string comparisons
// and not pointer-comparisons
std::string_view(VK_EXT_DEBUG_UTILS_EXTENSION_NAME)
) != instanceExtensions.end()) {
vk::DebugUtilsMessengerCreateInfoEXT debugCreateInfo{};
debugCreateInfo.messageSeverity = vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose | vk::DebugUtilsMessageSeverityFlagBitsEXT::eInfo |
vk::DebugUtilsMessageSeverityFlagBitsEXT::eError | vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning;
debugCreateInfo.messageType = vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance | vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation |
vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral;
debugCreateInfo.pfnUserCallback = &Vulkan::debugMessageCallback;
if (auto createResult = instance->createDebugUtilsMessengerEXTUnique(debugCreateInfo); createResult.result == vk::Result::eSuccess) {
debugMessenger = std::move(createResult.value);
} else {
Helpers::warn("Error registering debug messenger: %s", vk::to_string(createResult.result).c_str());
}
}
// Create surface
if (VkSurfaceKHR newSurface; SDL_Vulkan_CreateSurface(window, instance.get(), &newSurface)) {
surface.reset(newSurface);
} else {
Helpers::warn("Error creating Vulkan surface");
}
// Pick physical device
if (auto enumerateResult = instance->enumeratePhysicalDevices(); enumerateResult.result == vk::Result::eSuccess) {
std::vector<vk::PhysicalDevice> physicalDevices = std::move(enumerateResult.value);
std::vector<vk::PhysicalDevice>::iterator partitionEnd = physicalDevices.end();
// Prefer GPUs that can access the surface
const auto surfaceSupport = [this](const vk::PhysicalDevice& physicalDevice) -> bool {
const usize queueCount = physicalDevice.getQueueFamilyProperties().size();
for (usize queueIndex = 0; queueIndex < queueCount; ++queueIndex) {
if (auto supportResult = physicalDevice.getSurfaceSupportKHR(queueIndex, surface.get());
supportResult.result == vk::Result::eSuccess) {
return supportResult.value;
}
}
return false;
};
partitionEnd = std::stable_partition(physicalDevices.begin(), partitionEnd, surfaceSupport);
// Prefer Discrete GPUs
const auto isDiscrete = [](const vk::PhysicalDevice& physicalDevice) -> bool {
return physicalDevice.getProperties().deviceType == vk::PhysicalDeviceType::eDiscreteGpu;
};
partitionEnd = std::stable_partition(physicalDevices.begin(), partitionEnd, isDiscrete);
// Pick the "best" out of all of the previous criteria, preserving the order that the
// driver gave us the devices in(ex: optimus configuration)
physicalDevice = physicalDevices.front();
} else {
Helpers::panic("Error enumerating physical devices: %s\n", vk::to_string(enumerateResult.result).c_str());
}
// Get device queues
std::vector<vk::DeviceQueueCreateInfo> deviceQueueInfos;
{
const std::vector<vk::QueueFamilyProperties> queueFamilyProperties = physicalDevice.getQueueFamilyProperties();
// Get present queue family
for (usize queueFamilyIndex = 0; queueFamilyIndex < queueFamilyProperties.size(); ++queueFamilyIndex) {
if (auto supportResult = physicalDevice.getSurfaceSupportKHR(queueFamilyIndex, surface.get());
supportResult.result == vk::Result::eSuccess) {
if (supportResult.value) {
presentQueueFamily = queueFamilyIndex;
break;
}
}
}
static const float queuePriority = 1.0f;
graphicsQueueFamily = findQueueFamily(queueFamilyProperties, vk::QueueFlagBits::eGraphics);
computeQueueFamily = findQueueFamily(queueFamilyProperties, vk::QueueFlagBits::eCompute);
transferQueueFamily = findQueueFamily(queueFamilyProperties, vk::QueueFlagBits::eTransfer);
// Requests a singular queue for each unique queue-family
const std::unordered_set<u32> queueFamilyRequests = {presentQueueFamily, graphicsQueueFamily, computeQueueFamily, transferQueueFamily};
for (const u32 queueFamilyIndex : queueFamilyRequests) {
deviceQueueInfos.emplace_back(vk::DeviceQueueCreateInfo({}, queueFamilyIndex, 1, &queuePriority));
}
}
// Create Device
vk::DeviceCreateInfo deviceInfo = {};
static const char* deviceExtensions[] = {
VK_KHR_SWAPCHAIN_EXTENSION_NAME,
#if defined(__APPLE__)
"VK_KHR_portability_subset",
#endif
// VK_KHR_TIMELINE_SEMAPHORE_EXTENSION_NAME
};
deviceInfo.ppEnabledExtensionNames = deviceExtensions;
deviceInfo.enabledExtensionCount = std::size(deviceExtensions);
vk::StructureChain<vk::PhysicalDeviceFeatures2, vk::PhysicalDeviceTimelineSemaphoreFeatures> deviceFeatureChain = {};
auto& deviceFeatures = deviceFeatureChain.get<vk::PhysicalDeviceFeatures2>().features;
auto& deviceTimelineFeatures = deviceFeatureChain.get<vk::PhysicalDeviceTimelineSemaphoreFeatures>();
// deviceTimelineFeatures.timelineSemaphore = true;
deviceInfo.pNext = &deviceFeatureChain.get();
deviceInfo.setQueueCreateInfos(deviceQueueInfos);
if (auto createResult = physicalDevice.createDeviceUnique(deviceInfo); createResult.result == vk::Result::eSuccess) {
device = std::move(createResult.value);
} else {
Helpers::panic("Error creating logical device: %s\n", vk::to_string(createResult.result).c_str());
}
// Initialize device-specific function pointers
VULKAN_HPP_DEFAULT_DISPATCHER.init(device.get());
presentQueue = device->getQueue(presentQueueFamily, 0);
graphicsQueue = device->getQueue(presentQueueFamily, 0);
computeQueue = device->getQueue(computeQueueFamily, 0);
transferQueue = device->getQueue(transferQueueFamily, 0);
// Command pool
vk::CommandPoolCreateInfo commandPoolInfo = {};
commandPoolInfo.flags = vk::CommandPoolCreateFlagBits::eResetCommandBuffer;
if (auto createResult = device->createCommandPoolUnique(commandPoolInfo); createResult.result == vk::Result::eSuccess) {
commandPool = std::move(createResult.value);
} else {
Helpers::panic("Error creating command pool: %s\n", vk::to_string(createResult.result).c_str());
}
// Create swapchain
vk::Extent2D swapchainExtent;
{
int windowWidth, windowHeight;
SDL_Vulkan_GetDrawableSize(window, &windowWidth, &windowHeight);
swapchainExtent.width = windowWidth;
swapchainExtent.height = windowHeight;
}
recreateSwapchain(surface.get(), swapchainExtent);
}
void RendererVK::clearBuffer(u32 startAddress, u32 endAddress, u32 value, u32 control) {}
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) {}

View file

@ -0,0 +1,163 @@
#include "renderer_vk/vk_debug.hpp"
#include <cstdarg>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <memory>
#include "helpers.hpp"
static std::uint8_t severityColor(vk::DebugUtilsMessageSeverityFlagBitsEXT Severity) {
switch (Severity) {
case vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose: {
// Dark Gray
return 90u;
}
case vk::DebugUtilsMessageSeverityFlagBitsEXT::eInfo: {
// Light Gray
return 90u;
}
case vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning: {
// Light Magenta
return 95u;
}
case vk::DebugUtilsMessageSeverityFlagBitsEXT::eError: {
// Light red
return 91u;
}
}
// Default Foreground Color
return 39u;
}
static std::uint8_t messageTypeColor(vk::DebugUtilsMessageTypeFlagsEXT MessageType) {
if (MessageType & vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral) {
// Dim
return 2u;
}
if (MessageType & vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance) {
// Bold/Bright
return 1u;
}
if (MessageType & vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation) {
// Light Gray
return 90u;
}
// Default Foreground Color
return 39u;
}
namespace Vulkan {
static void debugMessageCallback(
vk::DebugUtilsMessageSeverityFlagBitsEXT MessageSeverity, vk::DebugUtilsMessageTypeFlagsEXT MessageType,
const vk::DebugUtilsMessengerCallbackDataEXT& CallbackData
) {
Helpers::debug_printf(
"\033[%um[vk][%s]: \033[%um%s\033[0m\n", severityColor(MessageSeverity), CallbackData.pMessageIdName, messageTypeColor(MessageType),
CallbackData.pMessage
);
}
VKAPI_ATTR VkBool32 VKAPI_CALL debugMessageCallback(
VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, VkDebugUtilsMessageTypeFlagsEXT messageType,
const VkDebugUtilsMessengerCallbackDataEXT* callbackData, void* userData
) {
debugMessageCallback(
vk::DebugUtilsMessageSeverityFlagBitsEXT(messageSeverity), vk::DebugUtilsMessageTypeFlagsEXT(messageType), *callbackData
);
return VK_FALSE;
}
#ifdef GPU_DEBUG_INFO
void setObjectName(vk::Device device, vk::ObjectType objectType, const void* objectHandle, const char* format, ...) {
va_list args;
va_start(args, format);
const auto nameLength = std::vsnprintf(nullptr, 0, format, args);
va_end(args);
if (nameLength < 0) {
// Invalid vsnprintf
return;
}
std::unique_ptr<char[]> objectName = std::make_unique<char[]>(std::size_t(nameLength) + 1u);
// Write formatted object name
va_start(args, format);
std::vsnprintf(objectName.get(), std::size_t(nameLength) + 1u, format, args);
va_end(args);
vk::DebugUtilsObjectNameInfoEXT nameInfo = {};
nameInfo.objectType = objectType;
nameInfo.objectHandle = reinterpret_cast<std::uintptr_t>(objectHandle);
nameInfo.pObjectName = objectName.get();
if (device.setDebugUtilsObjectNameEXT(nameInfo) != vk::Result::eSuccess) {
// Failed to set object name
}
}
void beginDebugLabel(vk::CommandBuffer commandBuffer, std::span<const float, 4> color, const char* format, ...) {
va_list args;
va_start(args, format);
const auto nameLength = std::vsnprintf(nullptr, 0, format, args);
va_end(args);
if (nameLength < 0) {
// Invalid vsnprintf
return;
}
std::unique_ptr<char[]> objectName = std::make_unique<char[]>(std::size_t(nameLength) + 1u);
// Write formatted object name
va_start(args, format);
std::vsnprintf(objectName.get(), std::size_t(nameLength) + 1u, format, args);
va_end(args);
vk::DebugUtilsLabelEXT labelInfo = {};
labelInfo.pLabelName = objectName.get();
labelInfo.color[0] = color[0];
labelInfo.color[1] = color[1];
labelInfo.color[2] = color[2];
labelInfo.color[3] = color[3];
commandBuffer.beginDebugUtilsLabelEXT(labelInfo);
}
void insertDebugLabel(vk::CommandBuffer commandBuffer, std::span<const float, 4> color, const char* format, ...) {
va_list args;
va_start(args, format);
const auto nameLength = std::vsnprintf(nullptr, 0, format, args);
va_end(args);
if (nameLength < 0) {
// Invalid vsnprintf
return;
}
std::unique_ptr<char[]> objectName = std::make_unique<char[]>(std::size_t(nameLength) + 1u);
// Write formatted object name
va_start(args, format);
std::vsnprintf(objectName.get(), std::size_t(nameLength) + 1u, format, args);
va_end(args);
vk::DebugUtilsLabelEXT labelInfo = {};
labelInfo.pLabelName = objectName.get();
labelInfo.color[0] = color[0];
labelInfo.color[1] = color[1];
labelInfo.color[2] = color[2];
labelInfo.color[3] = color[3];
commandBuffer.insertDebugUtilsLabelEXT(labelInfo);
}
void endDebugLabel(vk::CommandBuffer commandBuffer) { commandBuffer.endDebugUtilsLabelEXT(); }
#else
void setObjectName(vk::Device device, vk::ObjectType objectType, const void* objectHandle, const char* format, ...) {}
void beginDebugLabel(vk::CommandBuffer commandBuffer, std::span<const float, 4> color, const char* format, ...) {}
void insertDebugLabel(vk::CommandBuffer commandBuffer, std::span<const float, 4> color, const char* format, ...) {}
void endDebugLabel(vk::CommandBuffer commandBuffer) {}
#endif // GPU_DEBUG_INFO
} // namespace Vulkan

View file

@ -0,0 +1,3 @@
#include "renderer_vk/vulkan_api.hpp"
VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE;

View file

@ -3,7 +3,12 @@
namespace ACCommands {
enum : u32 {
SetClientVersion = 0x00400042
CreateDefaultConfig = 0x00010000,
CancelConnectAsync = 0x00070002,
CloseAsync = 0x00080004,
GetLastErrorCode = 0x000A0000,
RegisterDisconnectEvent = 0x00300004,
SetClientVersion = 0x00400042,
};
}
@ -12,15 +17,63 @@ 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");
mem.write32(messagePointer, IPC::responseHeader(0x0A, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write32(messagePointer + 8, 0); // Hopefully this means no error?
}
void ACService::setClientVersion(u32 messagePointer) {
u32 version = mem.read32(messagePointer + 4);
log("AC::SetClientVersion (version = %d)\n", version);
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 ACTCommands {
enum : u32 {
Initialize = 0x00010084,
GenerateUUID = 0x000D0040
GetAccountDataBlock = 0x000600C2,
GenerateUUID = 0x000D0040,
};
}
@ -14,6 +15,7 @@ void ACTService::handleSyncRequest(u32 messagePointer) {
const u32 command = mem.read32(messagePointer);
switch (command) {
case ACTCommands::GenerateUUID: generateUUID(messagePointer); break;
case ACTCommands::GetAccountDataBlock: getAccountDataBlock(messagePointer); break;
case ACTCommands::Initialize: initialize(messagePointer); break;
default: Helpers::panic("ACT service requested. Command: %08X\n", command);
}
@ -32,4 +34,17 @@ void ACTService::generateUUID(u32 messagePointer) {
// TODO: The header is probably wrong
mem.write32(messagePointer, IPC::responseHeader(0xD, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void ACTService::getAccountDataBlock(u32 messagePointer) {
log("ACT::GetAccountDataBlock (stubbed)\n");
const u32 size = mem.read32(messagePointer + 8);
const u32 blkID = mem.read32(messagePointer + 12);
const u32 outputPointer = mem.read32(messagePointer + 20);
// TODO: This header is probably also wrong
// Also we need to populate the data block here. Half of it is undocumented though >_<
mem.write32(messagePointer, IPC::responseHeader(0x6, 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,
@ -94,10 +96,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::preloadLibraryApplet(u32 messagePointer) {
@ -242,7 +258,7 @@ void APTService::getApplicationCpuTimeLimit(u32 messagePointer) {
void APTService::setScreencapPostPermission(u32 messagePointer) {
u32 perm = mem.read32(messagePointer + 4);
log("APT::SetScreencapPostPermission (perm = %d)\n");
log("APT::SetScreencapPostPermission (perm = %d)\n", perm);
mem.write32(messagePointer, IPC::responseHeader(0x55, 1, 0));
// Apparently only 1-3 are valid values, but I see 0 used in some games like Pokemon Rumble
@ -282,4 +298,4 @@ void APTService::getWirelessRebootInfo(u32 messagePointer) {
for (u32 i = 0; i < size; i++) {
mem.write8(messagePointer + 0x104 + i, 0); // Temporarily stub this until we add SetWirelessRebootInfo
}
}
}

View file

@ -6,14 +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,
};
}
@ -24,15 +34,26 @@ void BOSSService::reset() {
void BOSSService::handleSyncRequest(u32 messagePointer) {
const u32 command = mem.read32(messagePointer);
switch (command) {
case BOSSCommands::GetNsDataIdList: getNsDataIdList(messagePointer); break;
case BOSSCommands::CancelTask: cancelTask(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);
@ -46,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) {
@ -74,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));
@ -82,27 +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

@ -5,6 +5,8 @@
#include <array>
#include <bit>
#include <string>
#include <unordered_map>
namespace CFGCommands {
enum : u32 {
@ -12,7 +14,8 @@ namespace CFGCommands {
SecureInfoGetRegion = 0x00020000,
GenHashConsoleUnique = 0x00030040,
GetRegionCanadaUSA = 0x00040000,
GetSystemModel = 0x00050000
GetSystemModel = 0x00050000,
GetCountryCodeID = 0x000A0040,
};
}
@ -22,6 +25,7 @@ void CFGService::handleSyncRequest(u32 messagePointer) {
const u32 command = mem.read32(messagePointer);
switch (command) {
case CFGCommands::GetConfigInfoBlk2: [[likely]] getConfigInfoBlk2(messagePointer); break;
case CFGCommands::GetCountryCodeID: getCountryCodeID(messagePointer); break;
case CFGCommands::GetRegionCanadaUSA: getRegionCanadaUSA(messagePointer); break;
case CFGCommands::GetSystemModel: getSystemModel(messagePointer); break;
case CFGCommands::GenHashConsoleUnique: genUniqueConsoleHash(messagePointer); break;
@ -115,6 +119,8 @@ void CFGService::getConfigInfoBlk2(u32 messagePointer) {
for (u32 i = 0; i < size; i += 4) {
mem.write32(output + i, 0);
}
} else if (size == 4 && blockID == 0x170000) { // Miiverse access key
mem.write32(output, 0);
} else {
Helpers::panic("Unhandled GetConfigInfoBlk2 configuration. Size = %d, block = %X", size, blockID);
}
@ -128,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) {
@ -147,9 +153,57 @@ 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);
mem.write8(messagePointer + 8, ret);
}
constexpr u16 C(const char name[3]) { return name[0] | (name[1] << 8); }
static std::unordered_map<u16, u16> countryCodeToTableIDMap = {
{C("JP"), 1}, {C("AI"), 8}, {C("AG"), 9}, {C("AR"), 10}, {C("AW"), 11}, {C("BS"), 12}, {C("BB"), 13}, {C("BZ"), 14}, {C("BO"), 15},
{C("BR"), 16}, {C("VG"), 17}, {C("CA"), 18}, {C("KY"), 19}, {C("CL"), 20}, {C("CO"), 21}, {C("CR"), 22}, {C("DM"), 23}, {C("DO"), 24},
{C("EC"), 25}, {C("SV"), 26}, {C("GF"), 27}, {C("GD"), 28}, {C("GP"), 29}, {C("GT"), 30}, {C("GY"), 31}, {C("HT"), 32}, {C("HN"), 33},
{C("JM"), 34}, {C("MQ"), 35}, {C("MX"), 36}, {C("MS"), 37}, {C("AN"), 38}, {C("NI"), 39}, {C("PA"), 40}, {C("PY"), 41}, {C("PE"), 42},
{C("KN"), 43}, {C("LC"), 44}, {C("VC"), 45}, {C("SR"), 46}, {C("TT"), 47}, {C("TC"), 48}, {C("US"), 49}, {C("UY"), 50}, {C("VI"), 51},
{C("VE"), 52}, {C("AL"), 64}, {C("AU"), 65}, {C("AT"), 66}, {C("BE"), 67}, {C("BA"), 68}, {C("BW"), 69}, {C("BG"), 70}, {C("HR"), 71},
{C("CY"), 72}, {C("CZ"), 73}, {C("DK"), 74}, {C("EE"), 75}, {C("FI"), 76}, {C("FR"), 77}, {C("DE"), 78}, {C("GR"), 79}, {C("HU"), 80},
{C("IS"), 81}, {C("IE"), 82}, {C("IT"), 83}, {C("LV"), 84}, {C("LS"), 85}, {C("LI"), 86}, {C("LT"), 87}, {C("LU"), 88}, {C("MK"), 89},
{C("MT"), 90}, {C("ME"), 91}, {C("MZ"), 92}, {C("NA"), 93}, {C("NL"), 94}, {C("NZ"), 95}, {C("NO"), 96}, {C("PL"), 97}, {C("PT"), 98},
{C("RO"), 99}, {C("RU"), 100}, {C("RS"), 101}, {C("SK"), 102}, {C("SI"), 103}, {C("ZA"), 104}, {C("ES"), 105}, {C("SZ"), 106}, {C("SE"), 107},
{C("CH"), 108}, {C("TR"), 109}, {C("GB"), 110}, {C("ZM"), 111}, {C("ZW"), 112}, {C("AZ"), 113}, {C("MR"), 114}, {C("ML"), 115}, {C("NE"), 116},
{C("TD"), 117}, {C("SD"), 118}, {C("ER"), 119}, {C("DJ"), 120}, {C("SO"), 121}, {C("AD"), 122}, {C("GI"), 123}, {C("GG"), 124}, {C("IM"), 125},
{C("JE"), 126}, {C("MC"), 127}, {C("TW"), 128}, {C("KR"), 136}, {C("HK"), 144}, {C("MO"), 145}, {C("ID"), 152}, {C("SG"), 153}, {C("TH"), 154},
{C("PH"), 155}, {C("MY"), 156}, {C("CN"), 160}, {C("AE"), 168}, {C("IN"), 169}, {C("EG"), 170}, {C("OM"), 171}, {C("QA"), 172}, {C("KW"), 173},
{C("SA"), 174}, {C("SY"), 175}, {C("BH"), 176}, {C("JO"), 177}, {C("SM"), 184}, {C("VA"), 185}, {C("BM"), 186},
};
void CFGService::getCountryCodeID(u32 messagePointer) {
// Read the character code as a u16 instead of as ASCII, and use it to index the unordered_map above and get the result
const u16 characterCode = mem.read16(messagePointer + 4);
log("CFG::GetCountryCodeID (code = %04X)\n", characterCode);
mem.write32(messagePointer, IPC::responseHeader(0x0A, 2, 0));
// If the character code is valid, return its table ID and a success code
if (auto search = countryCodeToTableIDMap.find(characterCode); search != countryCodeToTableIDMap.end()) {
mem.write32(messagePointer + 4, Result::Success);
mem.write16(messagePointer + 8, search->second);
}
else {
Helpers::warn("CFG::GetCountryCodeID: Invalid country code %X", characterCode);
mem.write32(messagePointer + 4, Result::CFG::NotFound);
mem.write16(messagePointer + 8, 0xFF);
}
}

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,
@ -44,13 +46,14 @@ void GPUService::handleSyncRequest(u32 messagePointer) {
case ServiceCommands::FlushDataCache: flushDataCache(messagePointer); break;
case ServiceCommands::RegisterInterruptRelayQueue: registerInterruptRelayQueue(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::TriggerCmdReqQueue: [[likely]] triggerCmdReqQueue(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);
}
}
@ -124,15 +127,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;
}
}
@ -220,7 +220,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);
@ -230,7 +230,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);
@ -261,6 +261,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");
@ -283,7 +301,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;
@ -299,6 +317,28 @@ void GPUService::processCommandBuffer() {
}
}
static u32 VaddrToPaddr(u32 addr) {
if (addr >= VirtualAddrs::VramStart && addr < (VirtualAddrs::VramStart + VirtualAddrs::VramSize)) [[likely]] {
return addr - VirtualAddrs::VramStart + PhysicalAddrs::VRAM;
}
else if (addr >= VirtualAddrs::LinearHeapStartOld && addr < VirtualAddrs::LinearHeapEndOld) {
return addr - VirtualAddrs::LinearHeapStartOld + PhysicalAddrs::FCRAM;
}
else if (addr >= VirtualAddrs::LinearHeapStartNew && addr < VirtualAddrs::LinearHeapEndNew) {
return addr - VirtualAddrs::LinearHeapStartNew + PhysicalAddrs::FCRAM;
}
else if (addr == 0) {
return 0;
}
Helpers::warn("[GSP::GPU VaddrToPaddr] Unknown virtual address %08X", addr);
// Obviously garbage address
return 0xF3310932;
}
// Fill 2 GPU framebuffers, buf0 and buf1, using a specific word value
void GPUService::memoryFill(u32* cmd) {
u32 control = cmd[7];
@ -316,38 +356,16 @@ void GPUService::memoryFill(u32* cmd) {
u32 control1 = control >> 16;
if (start0 != 0) {
gpu.clearBuffer(start0, end0, value0, control0);
gpu.clearBuffer(VaddrToPaddr(start0), VaddrToPaddr(end0), value0, control0);
requestInterrupt(GPUInterrupt::PSC0);
}
if (start1 != 0) {
gpu.clearBuffer(start1, end1, value1, control1);
gpu.clearBuffer(VaddrToPaddr(start1), VaddrToPaddr(end1), value1, control1);
requestInterrupt(GPUInterrupt::PSC1);
}
}
static u32 VaddrToPaddr(u32 addr) {
if (addr >= VirtualAddrs::VramStart && addr < (VirtualAddrs::VramStart + VirtualAddrs::VramSize)) [[likely]] {
return addr - VirtualAddrs::VramStart + PhysicalAddrs::VRAM;
}
else if (addr >= VirtualAddrs::LinearHeapStartOld && addr < VirtualAddrs::LinearHeapEndOld) {
return addr - VirtualAddrs::LinearHeapStartOld + PhysicalAddrs::FCRAM;
}
else if (addr >= VirtualAddrs::LinearHeapStartNew && addr < VirtualAddrs::LinearHeapEndNew) {
return addr - VirtualAddrs::LinearHeapStartNew + PhysicalAddrs::FCRAM;
}
else if (addr == 0) {
return 0;
}
Helpers::warn("[GSP::GPU VaddrToPaddr] Unknown virtual address %08X", addr);
// Obviously garbage address
return 0xF3310932;
}
void GPUService::triggerDisplayTransfer(u32* cmd) {
const u32 inputAddr = VaddrToPaddr(cmd[1]);
const u32 outputAddr = VaddrToPaddr(cmd[2]);
@ -375,12 +393,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);
@ -390,8 +443,16 @@ 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,42 @@
#include "services/http.hpp"
#include "ipc.hpp"
#include "result/result.hpp"
namespace HTTPCommands {
enum : u32 {
Initialize = 0x00010044,
};
}
void HTTPService::reset() { initialized = false; }
void HTTPService::handleSyncRequest(u32 messagePointer) {
const u32 command = mem.read32(messagePointer);
switch (command) {
case HTTPCommands::Initialize: initialize(messagePointer); break;
default: Helpers::panic("HTTP service requested. Command: %08X\n", command);
}
}
void HTTPService::initialize(u32 messagePointer) {
const u32 postBufferSize = mem.read32(messagePointer + 4);
const u32 postMemoryBlockHandle = mem.read32(messagePointer + 20);
log("HTTP::Initialize (POST buffer size = %X, POST buffer memory block handle = %X)\n", postBufferSize, postMemoryBlockHandle);
mem.write32(messagePointer, IPC::responseHeader(0x01, 1, 0));
if (initialized) {
Helpers::warn("HTTP: Tried to initialize service while already initialized");
// TODO: Error code here
}
// 3DBrew: The provided POST buffer must be page-aligned (0x1000).
if (postBufferSize & 0xfff) {
Helpers::warn("HTTP: POST buffer size is not page-aligned");
}
initialized = true;
// We currently don't emulate HTTP properly. TODO: Prepare POST buffer here
mem.write32(messagePointer + 4, Result::Success);
}

View file

@ -1,5 +1,7 @@
#include "services/ir_user.hpp"
#include <cstddef>
#include "ipc.hpp"
#include "kernel.hpp"
@ -8,21 +10,30 @@ namespace IRUserCommands {
FinalizeIrnop = 0x00020000,
RequireConnection = 0x00060040,
Disconnect = 0x00090000,
GetReceiveEvent = 0x000A0000,
GetConnectionStatusEvent = 0x000C0000,
SendIrnop = 0x000D0042,
InitializeIrnopShared = 0x00180182
};
}
void IRUserService::reset() { connectionStatusEvent = std::nullopt; }
void IRUserService::reset() {
connectionStatusEvent = std::nullopt;
receiveEvent = std::nullopt;
sharedMemory = std::nullopt;
connectedDevice = false;
}
void IRUserService::handleSyncRequest(u32 messagePointer) {
const u32 command = mem.read32(messagePointer);
switch (command) {
case IRUserCommands::Disconnect: disconnect(messagePointer); break;
case IRUserCommands::FinalizeIrnop: finalizeIrnop(messagePointer); break;
case IRUserCommands::GetReceiveEvent: getReceiveEvent(messagePointer); break;
case IRUserCommands::GetConnectionStatusEvent: getConnectionStatusEvent(messagePointer); break;
case IRUserCommands::InitializeIrnopShared: initializeIrnopShared(messagePointer); break;
case IRUserCommands::RequireConnection: requireConnection(messagePointer); break;
case IRUserCommands::SendIrnop: sendIrnop(messagePointer); break;
default: Helpers::panic("ir:USER service requested. Command: %08X\n", command);
}
}
@ -40,6 +51,19 @@ void IRUserService::initializeIrnopShared(u32 messagePointer) {
log("IR:USER: InitializeIrnopShared (shared mem size = %08X, sharedMemHandle = %X) (stubbed)\n", sharedMemSize, sharedMemHandle);
Helpers::warn("Game is initializing IR:USER. If it explodes, this is probably why");
KernelObject* object = kernel.getObject(sharedMemHandle, KernelObjectType::MemoryBlock);
if (object == nullptr) {
Helpers::panic("IR::InitializeIrnopShared: Shared memory object does not exist");
}
MemoryBlock* memoryBlock = object->getData<MemoryBlock>();
sharedMemory = *memoryBlock;
// Set the initialized byte in shared mem to 1
mem.write8(memoryBlock->addr + offsetof(SharedMemoryStatus, isInitialized), 1);
mem.write64(memoryBlock->addr + 0x10, 0); // Initialize the receive buffer info to all 0s
mem.write64(memoryBlock->addr + 0x18, 0);
mem.write32(messagePointer, IPC::responseHeader(0x18, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
@ -47,6 +71,13 @@ void IRUserService::initializeIrnopShared(u32 messagePointer) {
void IRUserService::finalizeIrnop(u32 messagePointer) {
log("IR:USER: FinalizeIrnop\n");
if (connectedDevice) {
connectedDevice = false;
// This should also disconnect CirclePad Pro?
}
sharedMemory = std::nullopt;
// This should disconnect any connected device de-initialize the shared memory
mem.write32(messagePointer, IPC::responseHeader(0x2, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
@ -58,6 +89,7 @@ void IRUserService::getConnectionStatusEvent(u32 messagePointer) {
if (!connectionStatusEvent.has_value()) {
connectionStatusEvent = kernel.makeEvent(ResetType::OneShot);
}
//kernel.signalEvent(connectionStatusEvent.value()); // ??????????????
mem.write32(messagePointer, IPC::responseHeader(0xC, 1, 2));
mem.write32(messagePointer + 4, Result::Success);
@ -65,16 +97,72 @@ void IRUserService::getConnectionStatusEvent(u32 messagePointer) {
mem.write32(messagePointer + 12, connectionStatusEvent.value());
}
void IRUserService::getReceiveEvent(u32 messagePointer) {
log("IR:USER: GetReceiveEvent\n");
if (!receiveEvent.has_value()) {
receiveEvent = kernel.makeEvent(ResetType::OneShot);
}
mem.write32(messagePointer, IPC::responseHeader(0xA, 1, 2));
mem.write32(messagePointer + 4, Result::Success);
mem.write32(messagePointer + 8, 0x40000000);
// TOOD: Descriptor here
mem.write32(messagePointer + 12, receiveEvent.value());
}
void IRUserService::requireConnection(u32 messagePointer) {
const u8 deviceID = mem.read8(messagePointer + 4);
log("IR:USER: RequireConnection (device: %d)\n", deviceID);
// Reference: https://github.com/citra-emu/citra/blob/c10ffda91feb3476a861c47fb38641c1007b9d33/src/core/hle/service/ir/ir_user.cpp#L306
if (sharedMemory.has_value()) {
u32 sharedMemAddress = sharedMemory.value().addr;
if (deviceID == u8(DeviceID::CirclePadPro)) {
mem.write8(sharedMemAddress + offsetof(SharedMemoryStatus, connectionStatus), 2); // Citra uses 2 here but only 1 works??
mem.write8(sharedMemAddress + offsetof(SharedMemoryStatus, connectionRole), 2);
mem.write8(sharedMemAddress + offsetof(SharedMemoryStatus, isConnected), 1);
connectedDevice = true;
if (connectionStatusEvent.has_value()) {
kernel.signalEvent(connectionStatusEvent.value());
}
} else {
log("IR:USER: Unknown device %d\n", deviceID);
mem.write8(sharedMemAddress + offsetof(SharedMemoryStatus, connectionStatus), 1);
mem.write8(sharedMemAddress + offsetof(SharedMemoryStatus, connectionAttemptStatus), 2);
}
}
mem.write32(messagePointer, IPC::responseHeader(0x6, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void IRUserService::sendIrnop(u32 messagePointer) {
Helpers::panic("IR:USER: SendIrnop\n");
mem.write32(messagePointer + 4, Result::Success);
}
void IRUserService::disconnect(u32 messagePointer) {
log("IR:USER: Disconnect\n");
if (sharedMemory.has_value()) {
u32 sharedMemAddress = sharedMemory.value().addr;
mem.write8(sharedMemAddress + offsetof(SharedMemoryStatus, connectionStatus), 0);
mem.write8(sharedMemAddress + offsetof(SharedMemoryStatus, isConnected), 0);
}
// If there's a connected device, disconnect it and trigger the status event
if (connectedDevice) {
connectedDevice = false;
if (connectionStatusEvent.has_value()) {
kernel.signalEvent(connectionStatusEvent.value());
}
}
mem.write32(messagePointer, IPC::responseHeader(0x9, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}

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,17 +5,20 @@ namespace MICCommands {
enum : u32 {
MapSharedMem = 0x00010042,
StartSampling = 0x00030140,
StopSampling = 0x00050000,
SetGain = 0x00080040,
GetGain = 0x00090000,
SetPower = 0x000A0040,
SetIirFilter = 0x000C0042,
SetClamp = 0x000D0040,
CaptainToadFunction = 0x00100040
CaptainToadFunction = 0x00100040,
};
}
void MICService::reset() {
micEnabled = false;
shouldClamp = false;
isSampling = false;
gain = 0;
}
@ -26,8 +29,10 @@ void MICService::handleSyncRequest(u32 messagePointer) {
case MICCommands::MapSharedMem: mapSharedMem(messagePointer); break;
case MICCommands::SetClamp: setClamp(messagePointer); break;
case MICCommands::SetGain: setGain(messagePointer); break;
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);
}
@ -86,10 +91,28 @@ 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);
log("MIC::SetIirFilter (size = %X, pointer = %08X) (Stubbed)\n", size, pointer);
mem.write32(messagePointer, IPC::responseHeader(0x0C, 1, 2));
mem.write32(messagePointer + 4, Result::Success);
}
// Found in Captain Toad: Treasure Tracker
// This is what 3DBrew says:
// When the input value is 0, value 1 is written to an u8 MIC module state field.

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),
dlp_srvr(mem), dsp(mem, kernel), hid(mem, kernel), 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) {}
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), 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;
@ -25,16 +26,22 @@ void ServiceManager::reset() {
dlp_srvr.reset();
dsp.reset();
hid.reset();
http.reset();
ir_user.reset();
frd.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;
@ -94,22 +101,28 @@ 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 },
{ "http:C", KernelHandles::HTTP },
{ "ir:USER", KernelHandles::IR_USER },
{ "frd:u", KernelHandles::FRD },
{ "fs:USER", KernelHandles::FS },
{ "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
@ -173,24 +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);

41
src/discord_rpc.cpp Normal file
View file

@ -0,0 +1,41 @@
#ifdef PANDA3DS_ENABLE_DISCORD_RPC
#include "discord_rpc.hpp"
#include <cstring>
#include <ctime>
void Discord::RPC::init() {
DiscordEventHandlers handlers{};
Discord_Initialize("1138176975865909360", &handlers, 1, nullptr);
startTimestamp = time(nullptr);
enabled = true;
}
void Discord::RPC::update(Discord::RPCStatus status, const std::string& game) {
DiscordRichPresence rpc{};
if (status == Discord::RPCStatus::Playing) {
rpc.details = "Playing a game";
rpc.state = game.c_str();
} else {
rpc.details = "Idle";
}
rpc.largeImageKey = "pand";
rpc.largeImageText = "Panda3DS is a 3DS emulator for Windows, MacOS and Linux";
rpc.startTimestamp = startTimestamp;
Discord_UpdatePresence(&rpc);
}
void Discord::RPC::stop() {
if (enabled) {
enabled = false;
Discord_ClearPresence();
Discord_Shutdown();
}
}
#endif

View file

@ -1,8 +1,5 @@
#include "emulator.hpp"
#ifdef PANDA3DS_ENABLE_OPENGL
#include <glad/gl.h>
#endif
#ifdef _WIN32
#include <windows.h>
@ -14,7 +11,13 @@ __declspec(dllexport) DWORD AmdPowerXpressRequestHighPerformance = 1;
}
#endif
Emulator::Emulator() : kernel(cpu, memory, gpu), cpu(memory, kernel), gpu(memory, config), memory(cpu.getTicksRef()) {
Emulator::Emulator()
: config(std::filesystem::current_path() / "config.toml"), kernel(cpu, memory, gpu, config), cpu(memory, kernel), gpu(memory, config),
memory(cpu.getTicksRef(), config), cheats(memory, kernel.getServiceManager().getHID()), running(false), programRunning(false)
#ifdef PANDA3DS_ENABLE_HTTP_SERVER
, httpServer(this)
#endif
{
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) < 0) {
Helpers::panic("Failed to initialize SDL2");
}
@ -25,25 +28,48 @@ Emulator::Emulator() : kernel(cpu, memory, gpu), cpu(memory, kernel), gpu(memory
Helpers::warn("Failed to initialize SDL2 GameController: %s", SDL_GetError());
}
// We need OpenGL for software rendering or for OpenGL if it's enabled
bool needOpenGL = config.rendererType == RendererType::Software;
#ifdef PANDA3DS_ENABLE_OPENGL
// Request OpenGL 4.1 Core (Max available on MacOS)
// MacOS gets mad if we don't explicitly demand a core profile
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1);
window = SDL_CreateWindow("Alber", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, SDL_WINDOW_OPENGL);
needOpenGL = needOpenGL || (config.rendererType == RendererType::OpenGL);
#endif
if (window == nullptr) {
Helpers::panic("Window creation failed: %s", SDL_GetError());
#ifdef PANDA3DS_ENABLE_DISCORD_RPC
if (config.discordRpcEnabled) {
discordRpc.init();
updateDiscord();
}
#endif
if (needOpenGL) {
// Demand 3.3 core for software renderer, or 4.1 core for OpenGL renderer (max available on MacOS)
// MacOS gets mad if we don't explicitly demand a core profile
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, config.rendererType == RendererType::Software ? 3 : 4);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, config.rendererType == RendererType::Software ? 3 : 1);
window = SDL_CreateWindow("Alber", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, SDL_WINDOW_OPENGL);
if (window == nullptr) {
Helpers::panic("Window creation failed: %s", SDL_GetError());
}
glContext = SDL_GL_CreateContext(window);
if (glContext == nullptr) {
Helpers::panic("OpenGL context creation failed: %s", SDL_GetError());
}
if (!gladLoadGL(reinterpret_cast<GLADloadfunc>(SDL_GL_GetProcAddress))) {
Helpers::panic("OpenGL init failed: %s", SDL_GetError());
}
}
glContext = SDL_GL_CreateContext(window);
if (glContext == nullptr) {
Helpers::panic("OpenGL context creation failed: %s", SDL_GetError());
}
#ifdef PANDA3DS_ENABLE_VULKAN
if (config.rendererType == RendererType::Vulkan) {
window = SDL_CreateWindow("Alber", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, SDL_WINDOW_VULKAN);
if (!gladLoadGL(reinterpret_cast<GLADloadfunc>(SDL_GL_GetProcAddress))) {
Helpers::panic("OpenGL init failed: %s", SDL_GetError());
if (window == nullptr) {
Helpers::warn("Window creation failed: %s", SDL_GetError());
}
}
#endif
@ -56,11 +82,16 @@ Emulator::Emulator() : kernel(cpu, memory, gpu), cpu(memory, kernel), gpu(memory
}
}
config.load(std::filesystem::current_path() / "config.toml");
reset(ReloadOption::NoReload);
}
Emulator::~Emulator() { config.save(std::filesystem::current_path() / "config.toml"); }
Emulator::~Emulator() {
config.save(std::filesystem::current_path() / "config.toml");
#ifdef PANDA3DS_ENABLE_DISCORD_RPC
discordRpc.stop();
#endif
}
void Emulator::reset(ReloadOption reload) {
cpu.reset();
@ -73,6 +104,11 @@ void Emulator::reset(ReloadOption reload) {
// Otherwise resetting the kernel or cpu might nuke them
cpu.setReg(13, VirtualAddrs::StackTop); // Set initial SP
// We're resetting without reloading the ROM, so yeet cheats
if (reload == ReloadOption::NoReload) {
cheats.reset();
}
// If a ROM is active and we reset, with the reload option enabled then reload it.
// This is necessary to set up stack, executable memory, .data/.rodata/.bss all over again
if (reload == ReloadOption::Reload && romType != ROMType::None && romPath.has_value()) {
@ -90,24 +126,15 @@ void Emulator::step() {}
void Emulator::render() {}
void Emulator::run() {
programRunning = true;
while (programRunning) {
#ifdef PANDA3DS_ENABLE_HTTP_SERVER
httpServer.startHttpServer();
httpServer.processActions();
#endif
while (running) {
ServiceManager& srv = kernel.getServiceManager();
if (romType != ROMType::None) {
#ifdef PANDA3DS_ENABLE_HTTP_SERVER
pollHttpServer();
#endif
runFrame(); // Run 1 frame of instructions
gpu.display(); // Display graphics
// Send VBlank interrupts
srv.sendGPUInterrupt(GPUInterrupt::VBlank0);
srv.sendGPUInterrupt(GPUInterrupt::VBlank1);
}
runFrame();
HIDService& hid = kernel.getServiceManager().getHID();
SDL_Event event;
while (SDL_PollEvent(&event)) {
@ -116,48 +143,61 @@ void Emulator::run() {
switch (event.type) {
case SDL_QUIT:
printf("Bye :(\n");
running = false;
programRunning = false;
return;
case SDL_KEYDOWN:
if (romType == ROMType::None) break;
switch (event.key.keysym.sym) {
case SDLK_l: srv.pressKey(Keys::A); break;
case SDLK_k: srv.pressKey(Keys::B); break;
case SDLK_o: srv.pressKey(Keys::X); break;
case SDLK_i: srv.pressKey(Keys::Y); break;
case SDLK_l: hid.pressKey(Keys::A); break;
case SDLK_k: hid.pressKey(Keys::B); break;
case SDLK_o: hid.pressKey(Keys::X); break;
case SDLK_i: hid.pressKey(Keys::Y); break;
case SDLK_q: srv.pressKey(Keys::L); break;
case SDLK_p: srv.pressKey(Keys::R); break;
case SDLK_q: hid.pressKey(Keys::L); break;
case SDLK_p: hid.pressKey(Keys::R); break;
case SDLK_RIGHT: srv.pressKey(Keys::Right); break;
case SDLK_LEFT: srv.pressKey(Keys::Left); break;
case SDLK_UP: srv.pressKey(Keys::Up); break;
case SDLK_DOWN: srv.pressKey(Keys::Down); break;
case SDLK_RIGHT: hid.pressKey(Keys::Right); break;
case SDLK_LEFT: hid.pressKey(Keys::Left); break;
case SDLK_UP: hid.pressKey(Keys::Up); break;
case SDLK_DOWN: hid.pressKey(Keys::Down); break;
case SDLK_w:
srv.setCirclepadY(0x9C);
hid.setCirclepadY(0x9C);
keyboardAnalogY = true;
break;
case SDLK_a:
srv.setCirclepadX(-0x9C);
hid.setCirclepadX(-0x9C);
keyboardAnalogX = true;
break;
case SDLK_s:
srv.setCirclepadY(-0x9C);
hid.setCirclepadY(-0x9C);
keyboardAnalogY = true;
break;
case SDLK_d:
srv.setCirclepadX(0x9C);
hid.setCirclepadX(0x9C);
keyboardAnalogX = true;
break;
case SDLK_RETURN: srv.pressKey(Keys::Start); break;
case SDLK_BACKSPACE: srv.pressKey(Keys::Select); break;
case SDLK_RETURN: hid.pressKey(Keys::Start); break;
case SDLK_BACKSPACE: hid.pressKey(Keys::Select); break;
// Use the F4 button as a hot-key to pause or resume the emulator
// We can't use the audio play/pause buttons because it's annoying
case SDLK_F4: {
togglePause();
break;
}
// Use F5 as a reset button
case SDLK_F5: {
reset(ReloadOption::Reload);
break;
}
}
break;
@ -165,34 +205,34 @@ void Emulator::run() {
if (romType == ROMType::None) break;
switch (event.key.keysym.sym) {
case SDLK_l: srv.releaseKey(Keys::A); break;
case SDLK_k: srv.releaseKey(Keys::B); break;
case SDLK_o: srv.releaseKey(Keys::X); break;
case SDLK_i: srv.releaseKey(Keys::Y); break;
case SDLK_l: hid.releaseKey(Keys::A); break;
case SDLK_k: hid.releaseKey(Keys::B); break;
case SDLK_o: hid.releaseKey(Keys::X); break;
case SDLK_i: hid.releaseKey(Keys::Y); break;
case SDLK_q: srv.releaseKey(Keys::L); break;
case SDLK_p: srv.releaseKey(Keys::R); break;
case SDLK_q: hid.releaseKey(Keys::L); break;
case SDLK_p: hid.releaseKey(Keys::R); break;
case SDLK_RIGHT: srv.releaseKey(Keys::Right); break;
case SDLK_LEFT: srv.releaseKey(Keys::Left); break;
case SDLK_UP: srv.releaseKey(Keys::Up); break;
case SDLK_DOWN: srv.releaseKey(Keys::Down); break;
case SDLK_RIGHT: hid.releaseKey(Keys::Right); break;
case SDLK_LEFT: hid.releaseKey(Keys::Left); break;
case SDLK_UP: hid.releaseKey(Keys::Up); break;
case SDLK_DOWN: hid.releaseKey(Keys::Down); break;
// Err this is probably not ideal
case SDLK_w:
case SDLK_s:
srv.setCirclepadY(0);
hid.setCirclepadY(0);
keyboardAnalogY = false;
break;
case SDLK_a:
case SDLK_d:
srv.setCirclepadX(0);
hid.setCirclepadX(0);
keyboardAnalogX = false;
break;
case SDLK_RETURN: srv.releaseKey(Keys::Start); break;
case SDLK_BACKSPACE: srv.releaseKey(Keys::Select); break;
case SDLK_RETURN: hid.releaseKey(Keys::Start); break;
case SDLK_BACKSPACE: hid.releaseKey(Keys::Select); break;
}
break;
@ -209,9 +249,9 @@ void Emulator::run() {
u16 x_converted = static_cast<u16>(x) - 40;
u16 y_converted = static_cast<u16>(y) - 240;
srv.setTouchScreenPress(x_converted, y_converted);
hid.setTouchScreenPress(x_converted, y_converted);
} else {
srv.releaseTouchScreen();
hid.releaseTouchScreen();
}
} else if (event.button.button == SDL_BUTTON_RIGHT) {
holdingRightClick = true;
@ -223,7 +263,7 @@ void Emulator::run() {
if (romType == ROMType::None) break;
if (event.button.button == SDL_BUTTON_LEFT) {
srv.releaseTouchScreen();
hid.releaseTouchScreen();
} else if (event.button.button == SDL_BUTTON_RIGHT) {
holdingRightClick = false;
}
@ -266,9 +306,9 @@ void Emulator::run() {
if (key != 0) {
if (event.cbutton.state == SDL_PRESSED) {
srv.pressKey(key);
hid.pressKey(key);
} else {
srv.releaseKey(key);
hid.releaseKey(key);
}
}
break;
@ -276,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;
srv.setRoll(roll);
srv.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;
}
@ -315,27 +372,53 @@ void Emulator::run() {
// Avoid overriding the keyboard's circlepad input
if (abs(stickX) < deadzone && !keyboardAnalogX) {
srv.setCirclepadX(0);
hid.setCirclepadX(0);
} else {
srv.setCirclepadX(stickX / div);
hid.setCirclepadX(stickX / div);
}
if (abs(stickY) < deadzone && !keyboardAnalogY) {
srv.setCirclepadY(0);
hid.setCirclepadY(0);
} else {
srv.setCirclepadY(-(stickY / div));
hid.setCirclepadY(-(stickY / div));
}
}
srv.updateInputs(cpu.getTicks());
hid.updateInputs(cpu.getTicks());
}
// TODO: Should this be uncommented?
// kernel.evalReschedule();
// Update inputs in the HID module
SDL_GL_SwapWindow(window);
}
}
void Emulator::runFrame() { cpu.runFrame(); }
// Only resume if a ROM is properly loaded
void Emulator::resume() { running = (romType != ROMType::None); }
void Emulator::pause() { running = false; }
void Emulator::togglePause() { running ? pause() : resume(); }
void Emulator::runFrame() {
if (running) {
cpu.runFrame(); // Run 1 frame of instructions
gpu.display(); // Display graphics
// Send VBlank interrupts
ServiceManager& srv = kernel.getServiceManager();
srv.sendGPUInterrupt(GPUInterrupt::VBlank0);
srv.sendGPUInterrupt(GPUInterrupt::VBlank1);
// Run cheats if any are loaded
if (cheats.haveCheats()) [[unlikely]] {
cheats.run();
}
} else if (romType != ROMType::None) {
// If the emulator is not running and a game is loaded, we still want to display the framebuffer otherwise we will get weird
// double-buffering issues
gpu.display();
}
}
bool Emulator::loadROM(const std::filesystem::path& path) {
// Reset the emulator if we've already loaded a ROM
@ -377,11 +460,15 @@ bool Emulator::loadROM(const std::filesystem::path& path) {
if (success) {
romPath = path;
#ifdef PANDA3DS_ENABLE_DISCORD_RPC
updateDiscord();
#endif
} else {
romPath = std::nullopt;
romType = ROMType::None;
}
resume(); // Start the emulator
return success;
}
@ -431,38 +518,19 @@ bool Emulator::loadELF(std::ifstream& file) {
}
// Reset our graphics context and initialize the GPU's graphics context
void Emulator::initGraphicsContext() { gpu.initGraphicsContext(); }
void Emulator::initGraphicsContext() { gpu.initGraphicsContext(window); }
#ifdef PANDA3DS_ENABLE_HTTP_SERVER
void Emulator::pollHttpServer() {
std::scoped_lock lock(httpServer.actionMutex);
ServiceManager& srv = kernel.getServiceManager();
if (httpServer.pendingAction) {
switch (httpServer.action) {
case HttpAction::Screenshot: gpu.screenshot(HttpServer::httpServerScreenshotPath); break;
case HttpAction::PressKey:
if (httpServer.pendingKey != 0) {
srv.pressKey(httpServer.pendingKey);
httpServer.pendingKey = 0;
}
break;
case HttpAction::ReleaseKey:
if (httpServer.pendingKey != 0) {
srv.releaseKey(httpServer.pendingKey);
httpServer.pendingKey = 0;
}
break;
case HttpAction::None: break;
#ifdef PANDA3DS_ENABLE_DISCORD_RPC
void Emulator::updateDiscord() {
if (config.discordRpcEnabled) {
if (romType != ROMType::None) {
const auto name = romPath.value().stem();
discordRpc.update(Discord::RPCStatus::Playing, name.string());
} else {
discordRpc.update(Discord::RPCStatus::Idling, "");
}
httpServer.action = HttpAction::None;
httpServer.pendingAction = false;
httpServer.pendingAction.notify_all();
}
}
#endif
#else
void Emulator::updateDiscord() {}
#endif

View file

@ -0,0 +1,8 @@
#version 410 core
in vec2 UV;
out vec4 FragColor;
uniform sampler2D u_texture;
void main() {
FragColor = texture(u_texture, UV);
}

View file

@ -0,0 +1,23 @@
#version 410 core
out vec2 UV;
void main() {
const vec4 positions[4] = vec4[](
vec4(-1.0, 1.0, 1.0, 1.0), // Top-left
vec4(1.0, 1.0, 1.0, 1.0), // Top-right
vec4(-1.0, -1.0, 1.0, 1.0), // Bottom-left
vec4(1.0, -1.0, 1.0, 1.0) // Bottom-right
);
// The 3DS displays both screens' framebuffer rotated 90 deg counter clockwise
// So we adjust our texcoords accordingly
const vec2 texcoords[4] = vec2[](
vec2(1.0, 1.0), // Top-right
vec2(1.0, 0.0), // Bottom-right
vec2(0.0, 1.0), // Top-left
vec2(0.0, 0.0) // Bottom-left
);
gl_Position = positions[gl_VertexID];
UV = texcoords[gl_VertexID];
}

View file

@ -0,0 +1,417 @@
#version 410 core
in vec3 v_tangent;
in vec3 v_normal;
in vec3 v_bitangent;
in vec4 v_colour;
in vec3 v_texcoord0;
in vec2 v_texcoord1;
in vec3 v_view;
in vec2 v_texcoord2;
flat in vec4 v_textureEnvColor[6];
flat in vec4 v_textureEnvBufferColor;
out vec4 fragColour;
// TEV uniforms
uniform uint u_textureEnvSource[6];
uniform uint u_textureEnvOperand[6];
uniform uint u_textureEnvCombiner[6];
uniform uint u_textureEnvScale[6];
// Depth control uniforms
uniform float u_depthScale;
uniform float u_depthOffset;
uniform bool u_depthmapEnable;
uniform sampler2D u_tex0;
uniform sampler2D u_tex1;
uniform sampler2D u_tex2;
uniform sampler1DArray u_tex_lighting_lut;
uniform uint u_picaRegs[0x200 - 0x48];
// Helper so that the implementation of u_pica_regs can be changed later
uint readPicaReg(uint reg_addr) { return u_picaRegs[reg_addr - 0x48]; }
vec4 tevSources[16];
vec4 tevNextPreviousBuffer;
bool tevUnimplementedSourceFlag = false;
// OpenGL ES 1.1 reference pages for TEVs (this is what the PICA200 implements):
// https://registry.khronos.org/OpenGL-Refpages/es1.1/xhtml/glTexEnv.xml
vec4 tevFetchSource(uint src_id) {
if (src_id >= 6u && src_id < 13u) {
tevUnimplementedSourceFlag = true;
}
return tevSources[src_id];
}
vec4 tevGetColorAndAlphaSource(int tev_id, int src_id) {
vec4 result;
vec4 colorSource = tevFetchSource((u_textureEnvSource[tev_id] >> (src_id * 4)) & 15u);
vec4 alphaSource = tevFetchSource((u_textureEnvSource[tev_id] >> (src_id * 4 + 16)) & 15u);
uint colorOperand = (u_textureEnvOperand[tev_id] >> (src_id * 4)) & 15u;
uint alphaOperand = (u_textureEnvOperand[tev_id] >> (12 + src_id * 4)) & 7u;
// TODO: figure out what the undocumented values do
switch (colorOperand) {
case 0u: result.rgb = colorSource.rgb; break; // Source color
case 1u: result.rgb = 1.0 - colorSource.rgb; break; // One minus source color
case 2u: result.rgb = vec3(colorSource.a); break; // Source alpha
case 3u: result.rgb = vec3(1.0 - colorSource.a); break; // One minus source alpha
case 4u: result.rgb = vec3(colorSource.r); break; // Source red
case 5u: result.rgb = vec3(1.0 - colorSource.r); break; // One minus source red
case 8u: result.rgb = vec3(colorSource.g); break; // Source green
case 9u: result.rgb = vec3(1.0 - colorSource.g); break; // One minus source green
case 12u: result.rgb = vec3(colorSource.b); break; // Source blue
case 13u: result.rgb = vec3(1.0 - colorSource.b); break; // One minus source blue
default: break;
}
// TODO: figure out what the undocumented values do
switch (alphaOperand) {
case 0u: result.a = alphaSource.a; break; // Source alpha
case 1u: result.a = 1.0 - alphaSource.a; break; // One minus source alpha
case 2u: result.a = alphaSource.r; break; // Source red
case 3u: result.a = 1.0 - alphaSource.r; break; // One minus source red
case 4u: result.a = alphaSource.g; break; // Source green
case 5u: result.a = 1.0 - alphaSource.g; break; // One minus source green
case 6u: result.a = alphaSource.b; break; // Source blue
case 7u: result.a = 1.0 - alphaSource.b; break; // One minus source blue
default: break;
}
return result;
}
vec4 tevCalculateCombiner(int tev_id) {
vec4 source0 = tevGetColorAndAlphaSource(tev_id, 0);
vec4 source1 = tevGetColorAndAlphaSource(tev_id, 1);
vec4 source2 = tevGetColorAndAlphaSource(tev_id, 2);
uint colorCombine = u_textureEnvCombiner[tev_id] & 15u;
uint alphaCombine = (u_textureEnvCombiner[tev_id] >> 16) & 15u;
vec4 result = vec4(1.0);
// TODO: figure out what the undocumented values do
switch (colorCombine) {
case 0u: result.rgb = source0.rgb; break; // Replace
case 1u: result.rgb = source0.rgb * source1.rgb; break; // Modulate
case 2u: result.rgb = min(vec3(1.0), source0.rgb + source1.rgb); break; // Add
case 3u: result.rgb = clamp(source0.rgb + source1.rgb - 0.5, 0.0, 1.0); break; // Add signed
case 4u: result.rgb = mix(source1.rgb, source0.rgb, source2.rgb); break; // Interpolate
case 5u: result.rgb = max(source0.rgb - source1.rgb, 0.0); break; // Subtract
case 6u: result.rgb = vec3(4.0 * dot(source0.rgb - 0.5, source1.rgb - 0.5)); break; // Dot3 RGB
case 7u: result = vec4(4.0 * dot(source0.rgb - 0.5, source1.rgb - 0.5)); break; // Dot3 RGBA
case 8u: result.rgb = min(source0.rgb * source1.rgb + source2.rgb, 1.0); break; // Multiply then add
case 9u: result.rgb = min((source0.rgb + source1.rgb) * source2.rgb, 1.0); break; // Add then multiply
default: break;
}
if (colorCombine != 7u) { // The color combiner also writes the alpha channel in the "Dot3 RGBA" mode.
// TODO: figure out what the undocumented values do
// TODO: test if the alpha combiner supports all the same modes as the color combiner.
switch (alphaCombine) {
case 0u: result.a = source0.a; break; // Replace
case 1u: result.a = source0.a * source1.a; break; // Modulate
case 2u: result.a = min(1.0, source0.a + source1.a); break; // Add
case 3u: result.a = clamp(source0.a + source1.a - 0.5, 0.0, 1.0); break; // Add signed
case 4u: result.a = mix(source1.a, source0.a, source2.a); break; // Interpolate
case 5u: result.a = max(0.0, source0.a - source1.a); break; // Subtract
case 8u: result.a = min(1.0, source0.a * source1.a + source2.a); break; // Multiply then add
case 9u: result.a = min(1.0, (source0.a + source1.a) * source2.a); break; // Add then multiply
default: break;
}
}
result.rgb *= float(1 << (u_textureEnvScale[tev_id] & 3u));
result.a *= float(1 << ((u_textureEnvScale[tev_id] >> 16) & 3u));
return result;
}
#define D0_LUT 0u
#define D1_LUT 1u
#define SP_LUT 2u
#define FR_LUT 3u
#define RB_LUT 4u
#define RG_LUT 5u
#define RR_LUT 6u
float lutLookup(uint lut, uint light, float value) {
if (lut >= FR_LUT && lut <= RR_LUT) lut -= 1;
if (lut == SP_LUT) lut = light + 8;
return texture(u_tex_lighting_lut, vec2(value, lut)).r;
}
vec3 regToColor(uint reg) {
// Normalization scale to convert from [0...255] to [0.0...1.0]
const float scale = 1.0 / 255.0;
return scale * vec3(float(bitfieldExtract(reg, 20, 8)), float(bitfieldExtract(reg, 10, 8)), float(bitfieldExtract(reg, 00, 8)));
}
// Convert an arbitrary-width floating point literal to an f32
float decodeFP(uint hex, uint E, uint M) {
uint width = M + E + 1u;
uint bias = 128u - (1u << (E - 1u));
uint exponent = (hex >> M) & ((1u << E) - 1u);
uint mantissa = hex & ((1u << M) - 1u);
uint sign = (hex >> (E + M)) << 31u;
if ((hex & ((1u << (width - 1u)) - 1u)) != 0) {
if (exponent == (1u << E) - 1u)
exponent = 255u;
else
exponent += bias;
hex = sign | (mantissa << (23u - M)) | (exponent << 23u);
} else {
hex = sign;
}
return uintBitsToFloat(hex);
}
// Implements the following algorthm: https://mathb.in/26766
void calcLighting(out vec4 primary_color, out vec4 secondary_color) {
// Quaternions describe a transformation from surface-local space to eye space.
// In surface-local space, by definition (and up to permutation) the normal vector is (0,0,1),
// the tangent vector is (1,0,0), and the bitangent vector is (0,1,0).
vec3 normal = normalize(v_normal);
vec3 tangent = normalize(v_tangent);
vec3 bitangent = normalize(v_bitangent);
vec3 view = normalize(v_view);
uint GPUREG_LIGHTING_ENABLE = readPicaReg(0x008F);
if (bitfieldExtract(GPUREG_LIGHTING_ENABLE, 0, 1) == 0) {
primary_color = secondary_color = vec4(1.0);
return;
}
uint GPUREG_LIGHTING_AMBIENT = readPicaReg(0x01C0);
uint GPUREG_LIGHTING_NUM_LIGHTS = (readPicaReg(0x01C2) & 0x7u) + 1;
uint GPUREG_LIGHTING_LIGHT_PERMUTATION = readPicaReg(0x01D9);
primary_color = vec4(vec3(0.0), 1.0);
secondary_color = vec4(vec3(0.0), 1.0);
primary_color.rgb += regToColor(GPUREG_LIGHTING_AMBIENT);
uint GPUREG_LIGHTING_LUTINPUT_ABS = readPicaReg(0x01D0);
uint GPUREG_LIGHTING_LUTINPUT_SELECT = readPicaReg(0x01D1);
uint GPUREG_LIGHTING_CONFIG0 = readPicaReg(0x01C3);
uint GPUREG_LIGHTING_CONFIG1 = readPicaReg(0x01C4);
uint GPUREG_LIGHTING_LUTINPUT_SCALE = readPicaReg(0x01D2);
float d[7];
bool error_unimpl = false;
for (uint i = 0; i < GPUREG_LIGHTING_NUM_LIGHTS; i++) {
uint light_id = bitfieldExtract(GPUREG_LIGHTING_LIGHT_PERMUTATION, int(i * 3), 3);
uint GPUREG_LIGHTi_SPECULAR0 = readPicaReg(0x0140 + 0x10 * light_id);
uint GPUREG_LIGHTi_SPECULAR1 = readPicaReg(0x0141 + 0x10 * light_id);
uint GPUREG_LIGHTi_DIFFUSE = readPicaReg(0x0142 + 0x10 * light_id);
uint GPUREG_LIGHTi_AMBIENT = readPicaReg(0x0143 + 0x10 * light_id);
uint GPUREG_LIGHTi_VECTOR_LOW = readPicaReg(0x0144 + 0x10 * light_id);
uint GPUREG_LIGHTi_VECTOR_HIGH = readPicaReg(0x0145 + 0x10 * light_id);
uint GPUREG_LIGHTi_CONFIG = readPicaReg(0x0149 + 0x10 * light_id);
vec3 light_vector = normalize(vec3(
decodeFP(bitfieldExtract(GPUREG_LIGHTi_VECTOR_LOW, 0, 16), 5, 10), decodeFP(bitfieldExtract(GPUREG_LIGHTi_VECTOR_LOW, 16, 16), 5, 10),
decodeFP(bitfieldExtract(GPUREG_LIGHTi_VECTOR_HIGH, 0, 16), 5, 10)
));
vec3 half_vector;
// Positional Light
if (bitfieldExtract(GPUREG_LIGHTi_CONFIG, 0, 1) == 0) {
// error_unimpl = true;
half_vector = normalize(normalize(light_vector + v_view) + view);
}
// Directional light
else {
half_vector = normalize(normalize(light_vector) + view);
}
for (int c = 0; c < 7; c++) {
if (bitfieldExtract(GPUREG_LIGHTING_CONFIG1, 16 + c, 1) == 0) {
uint scale_id = bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_SCALE, c * 4, 3);
float scale = float(1u << scale_id);
if (scale_id >= 6u) scale /= 256.0;
uint input_id = bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_SELECT, c * 4, 3);
if (input_id == 0u)
d[c] = dot(normal, half_vector);
else if (input_id == 1u)
d[c] = dot(view, half_vector);
else if (input_id == 2u)
d[c] = dot(normal, view);
else if (input_id == 3u)
d[c] = dot(light_vector, normal);
else if (input_id == 4u) {
uint GPUREG_LIGHTi_SPOTDIR_LOW = readPicaReg(0x0146 + 0x10 * light_id);
uint GPUREG_LIGHTi_SPOTDIR_HIGH = readPicaReg(0x0147 + 0x10 * light_id);
vec3 spot_light_vector = normalize(vec3(
decodeFP(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 0, 16), 1, 11),
decodeFP(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 16, 16), 1, 11),
decodeFP(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_HIGH, 0, 16), 1, 11)
));
d[c] = dot(-light_vector, spot_light_vector); // -L dot P (aka Spotlight aka SP);
} else if (input_id == 5u) {
d[c] = 1.0; // TODO: cos <greek symbol> (aka CP);
error_unimpl = true;
} else {
d[c] = 1.0;
}
d[c] = lutLookup(c, light_id, d[c] * 0.5 + 0.5) * scale;
if (bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_ABS, 2 * c, 1) != 0u) d[c] = abs(d[c]);
} else {
d[c] = 1.0;
}
}
uint lookup_config = bitfieldExtract(GPUREG_LIGHTi_CONFIG, 4, 4);
if (lookup_config == 0) {
d[D1_LUT] = 0.0;
d[FR_LUT] = 0.0;
d[RG_LUT] = d[RB_LUT] = d[RR_LUT];
} else if (lookup_config == 1) {
d[D0_LUT] = 0.0;
d[D1_LUT] = 0.0;
d[RG_LUT] = d[RB_LUT] = d[RR_LUT];
} else if (lookup_config == 2) {
d[FR_LUT] = 0.0;
d[SP_LUT] = 0.0;
d[RG_LUT] = d[RB_LUT] = d[RR_LUT];
} else if (lookup_config == 3) {
d[SP_LUT] = 0.0;
d[RG_LUT] = d[RB_LUT] = d[RR_LUT] = 1.0;
} else if (lookup_config == 4) {
d[FR_LUT] = 0.0;
} else if (lookup_config == 5) {
d[D1_LUT] = 0.0;
} else if (lookup_config == 6) {
d[RG_LUT] = d[RB_LUT] = d[RR_LUT];
}
float distance_factor = 1.0; // a
float indirect_factor = 1.0; // fi
float shadow_factor = 1.0; // o
float NdotL = dot(normal, light_vector); // Li dot N
// Two sided diffuse
if (bitfieldExtract(GPUREG_LIGHTi_CONFIG, 1, 1) == 0)
NdotL = max(0.0, NdotL);
else
NdotL = abs(NdotL);
float light_factor = distance_factor * d[SP_LUT] * indirect_factor * shadow_factor;
primary_color.rgb += light_factor * (regToColor(GPUREG_LIGHTi_AMBIENT) + regToColor(GPUREG_LIGHTi_DIFFUSE) * NdotL);
secondary_color.rgb += light_factor * (regToColor(GPUREG_LIGHTi_SPECULAR0) * d[D0_LUT] +
regToColor(GPUREG_LIGHTi_SPECULAR1) * d[D1_LUT] * vec3(d[RR_LUT], d[RG_LUT], d[RB_LUT]));
}
uint fresnel_output1 = bitfieldExtract(GPUREG_LIGHTING_CONFIG0, 2, 1);
uint fresnel_output2 = bitfieldExtract(GPUREG_LIGHTING_CONFIG0, 3, 1);
if (fresnel_output1 == 1u) primary_color.a = d[FR_LUT];
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);
}
}
void main() {
// TODO: what do invalid sources and disabled textures read as?
// And what does the "previous combiner" source read initially?
tevSources[0] = v_colour; // Primary/vertex color
calcLighting(tevSources[1], tevSources[2]);
uint textureConfig = readPicaReg(0x80);
vec2 tex2UV = (textureConfig & (1u << 13)) != 0u ? v_texcoord1 : v_texcoord2;
if ((textureConfig & 1u) != 0u) tevSources[3] = texture(u_tex0, v_texcoord0.xy);
if ((textureConfig & 2u) != 0u) tevSources[4] = texture(u_tex1, v_texcoord1);
if ((textureConfig & 4u) != 0u) tevSources[5] = texture(u_tex2, tex2UV);
tevSources[13] = vec4(0.0); // Previous buffer
tevSources[15] = vec4(0.0); // Previous combiner
tevNextPreviousBuffer = v_textureEnvBufferColor;
uint textureEnvUpdateBuffer = readPicaReg(0xE0);
for (int i = 0; i < 6; i++) {
tevSources[14] = v_textureEnvColor[i]; // Constant color
tevSources[15] = tevCalculateCombiner(i);
tevSources[13] = tevNextPreviousBuffer;
if (i < 4) {
if ((textureEnvUpdateBuffer & (0x100u << i)) != 0u) {
tevNextPreviousBuffer.rgb = tevSources[15].rgb;
}
if ((textureEnvUpdateBuffer & (0x1000u << i)) != 0u) {
tevNextPreviousBuffer.a = tevSources[15].a;
}
}
}
fragColour = tevSources[15];
if (tevUnimplementedSourceFlag) {
// fragColour = vec4(1.0, 0.0, 1.0, 1.0);
}
// fragColour.rg = texture(u_tex_lighting_lut,vec2(gl_FragCoord.x/200.,float(int(gl_FragCoord.y/2)%24))).rr;
// Get original depth value by converting from [near, far] = [0, 1] to [-1, 1]
// We do this by converting to [0, 2] first and subtracting 1 to go to [-1, 1]
float z_over_w = gl_FragCoord.z * 2.0f - 1.0f;
float depth = z_over_w * u_depthScale + u_depthOffset;
if (!u_depthmapEnable) // Divide z by w if depthmap enable == 0 (ie using W-buffering)
depth /= gl_FragCoord.w;
// Write final fragment depth
gl_FragDepth = depth;
// Perform alpha test
uint alphaControl = readPicaReg(0x104);
if ((alphaControl & 1u) != 0u) { // Check if alpha test is on
uint func = (alphaControl >> 4u) & 7u;
float reference = float((alphaControl >> 8u) & 0xffu) / 255.0;
float alpha = fragColour.a;
switch (func) {
case 0: discard; // Never pass alpha test
case 1: break; // Always pass alpha test
case 2: // Pass if equal
if (alpha != reference) discard;
break;
case 3: // Pass if not equal
if (alpha == reference) discard;
break;
case 4: // Pass if less than
if (alpha >= reference) discard;
break;
case 5: // Pass if less than or equal
if (alpha > reference) discard;
break;
case 6: // Pass if greater than
if (alpha <= reference) discard;
break;
case 7: // Pass if greater than or equal
if (alpha < reference) discard;
break;
}
}
}

View file

@ -0,0 +1,97 @@
#version 410 core
layout(location = 0) in vec4 a_coords;
layout(location = 1) in vec4 a_quaternion;
layout(location = 2) in vec4 a_vertexColour;
layout(location = 3) in vec2 a_texcoord0;
layout(location = 4) in vec2 a_texcoord1;
layout(location = 5) in float a_texcoord0_w;
layout(location = 6) in vec3 a_view;
layout(location = 7) in vec2 a_texcoord2;
out vec3 v_normal;
out vec3 v_tangent;
out vec3 v_bitangent;
out vec4 v_colour;
out vec3 v_texcoord0;
out vec2 v_texcoord1;
out vec3 v_view;
out vec2 v_texcoord2;
flat out vec4 v_textureEnvColor[6];
flat out vec4 v_textureEnvBufferColor;
out float gl_ClipDistance[2];
// TEV uniforms
uniform uint u_textureEnvColor[6];
uniform uint u_picaRegs[0x200 - 0x48];
// Helper so that the implementation of u_pica_regs can be changed later
uint readPicaReg(uint reg_addr) { return u_picaRegs[reg_addr - 0x48]; }
vec4 abgr8888ToVec4(uint abgr) {
const float scale = 1.0 / 255.0;
return scale * vec4(float(abgr & 0xffu), float((abgr >> 8) & 0xffu), float((abgr >> 16) & 0xffu), float(abgr >> 24));
}
vec3 rotateVec3ByQuaternion(vec3 v, vec4 q) {
vec3 u = q.xyz;
float s = q.w;
return 2.0 * dot(u, v) * u + (s * s - dot(u, u)) * v + 2.0 * s * cross(u, v);
}
// Convert an arbitrary-width floating point literal to an f32
float decodeFP(uint hex, uint E, uint M) {
uint width = M + E + 1u;
uint bias = 128u - (1u << (E - 1u));
uint exponent = (hex >> M) & ((1u << E) - 1u);
uint mantissa = hex & ((1u << M) - 1u);
uint sign = (hex >> (E + M)) << 31u;
if ((hex & ((1u << (width - 1u)) - 1u)) != 0) {
if (exponent == (1u << E) - 1u)
exponent = 255u;
else
exponent += bias;
hex = sign | (mantissa << (23u - M)) | (exponent << 23u);
} else {
hex = sign;
}
return uintBitsToFloat(hex);
}
void main() {
gl_Position = a_coords;
v_colour = a_vertexColour;
// Flip y axis of UVs because OpenGL uses an inverted y for texture sampling compared to the PICA
v_texcoord0 = vec3(a_texcoord0.x, 1.0 - a_texcoord0.y, a_texcoord0_w);
v_texcoord1 = vec2(a_texcoord1.x, 1.0 - a_texcoord1.y);
v_texcoord2 = vec2(a_texcoord2.x, 1.0 - a_texcoord2.y);
v_view = a_view;
v_normal = normalize(rotateVec3ByQuaternion(vec3(0.0, 0.0, 1.0), a_quaternion));
v_tangent = normalize(rotateVec3ByQuaternion(vec3(1.0, 0.0, 0.0), a_quaternion));
v_bitangent = normalize(rotateVec3ByQuaternion(vec3(0.0, 1.0, 0.0), a_quaternion));
for (int i = 0; i < 6; i++) {
v_textureEnvColor[i] = abgr8888ToVec4(u_textureEnvColor[i]);
}
v_textureEnvBufferColor = abgr8888ToVec4(readPicaReg(0xFD));
// Parse clipping plane registers
// The plane registers describe a clipping plane in the form of Ax + By + Cz + D = 0
// With n = (A, B, C) being the normal vector and D being the origin point distance
// Therefore, for the second clipping plane, we can just pass the dot product of the clip vector and the input coordinates to gl_ClipDistance[1]
vec4 clipData = vec4(
decodeFP(readPicaReg(0x48) & 0xffffffu, 7, 16), decodeFP(readPicaReg(0x49) & 0xffffffu, 7, 16),
decodeFP(readPicaReg(0x4A) & 0xffffffu, 7, 16), decodeFP(readPicaReg(0x4B) & 0xffffffu, 7, 16)
);
// There's also another, always-on clipping plane based on vertex z
gl_ClipDistance[0] = -a_coords.z;
gl_ClipDistance[1] = dot(clipData, a_coords);
}

356
src/http_server.cpp Normal file
View file

@ -0,0 +1,356 @@
#ifdef PANDA3DS_ENABLE_HTTP_SERVER
#include "http_server.hpp"
#include <fstream>
#include <sstream>
#include <string>
#include <system_error>
#include <thread>
#include <vector>
#include "emulator.hpp"
#include "helpers.hpp"
#include "httplib.h"
class HttpActionScreenshot : public HttpAction {
DeferredResponseWrapper& response;
public:
HttpActionScreenshot(DeferredResponseWrapper& response) : HttpAction(HttpActionType::Screenshot), response(response) {}
DeferredResponseWrapper& getResponse() { return response; }
};
class HttpActionTogglePause : public HttpAction {
public:
HttpActionTogglePause() : HttpAction(HttpActionType::TogglePause) {}
};
class HttpActionReset : public HttpAction {
public:
HttpActionReset() : HttpAction(HttpActionType::Reset) {}
};
class HttpActionKey : public HttpAction {
u32 key;
bool state;
public:
HttpActionKey(u32 key, bool state) : HttpAction(HttpActionType::Key), key(key), state(state) {}
u32 getKey() const { return key; }
bool getState() const { return state; }
};
class HttpActionLoadRom : public HttpAction {
DeferredResponseWrapper& response;
const std::filesystem::path& path;
bool paused;
public:
HttpActionLoadRom(DeferredResponseWrapper& response, const std::filesystem::path& path, bool paused)
: HttpAction(HttpActionType::LoadRom), response(response), path(path), paused(paused) {}
DeferredResponseWrapper& getResponse() { return response; }
const std::filesystem::path& getPath() const { return path; }
bool getPaused() const { return paused; }
};
class HttpActionStep : public HttpAction {
DeferredResponseWrapper& response;
int frames;
public:
HttpActionStep(DeferredResponseWrapper& response, int frames)
: HttpAction(HttpActionType::Step), response(response), frames(frames) {}
DeferredResponseWrapper& getResponse() { return response; }
int getFrames() const { return frames; }
};
std::unique_ptr<HttpAction> HttpAction::createScreenshotAction(DeferredResponseWrapper& response) {
return std::make_unique<HttpActionScreenshot>(response);
}
std::unique_ptr<HttpAction> HttpAction::createKeyAction(u32 key, bool state) { return std::make_unique<HttpActionKey>(key, state); }
std::unique_ptr<HttpAction> HttpAction::createTogglePauseAction() { return std::make_unique<HttpActionTogglePause>(); }
std::unique_ptr<HttpAction> HttpAction::createResetAction() { return std::make_unique<HttpActionReset>(); }
std::unique_ptr<HttpAction> HttpAction::createLoadRomAction(DeferredResponseWrapper& response, const std::filesystem::path& path, bool paused) {
return std::make_unique<HttpActionLoadRom>(response, path, paused);
}
std::unique_ptr<HttpAction> HttpAction::createStepAction(DeferredResponseWrapper& response, int frames) {
return std::make_unique<HttpActionStep>(response, frames);
}
HttpServer::HttpServer(Emulator* emulator)
: emulator(emulator), server(std::make_unique<httplib::Server>()), keyMap({
{"A", {HID::Keys::A}},
{"B", {HID::Keys::B}},
{"Select", {HID::Keys::Select}},
{"Start", {HID::Keys::Start}},
{"Right", {HID::Keys::Right}},
{"Left", {HID::Keys::Left}},
{"Up", {HID::Keys::Up}},
{"Down", {HID::Keys::Down}},
{"R", {HID::Keys::R}},
{"L", {HID::Keys::L}},
{"X", {HID::Keys::X}},
{"Y", {HID::Keys::Y}},
}) {
httpServerThread = std::thread(&HttpServer::startHttpServer, this);
}
HttpServer::~HttpServer() {
printf("Stopping http server...\n");
server->stop();
if (httpServerThread.joinable()) {
httpServerThread.join();
}
}
void HttpServer::pushAction(std::unique_ptr<HttpAction> action) {
std::scoped_lock lock(actionQueueMutex);
actionQueue.push(std::move(action));
}
void HttpServer::startHttpServer() {
server->set_tcp_nodelay(true);
server->Get("/ping", [](const httplib::Request&, httplib::Response& response) { response.set_content("pong", "text/plain"); });
server->Get("/screen", [this](const httplib::Request&, httplib::Response& response) {
// TODO: make the below a DeferredResponseWrapper function
DeferredResponseWrapper wrapper(response);
// Lock the mutex before pushing the action to ensure that the condition variable is not notified before we wait on it
std::unique_lock lock(wrapper.mutex);
pushAction(HttpAction::createScreenshotAction(wrapper));
wrapper.cv.wait(lock, [&wrapper] { return wrapper.ready; });
});
server->Get("/input", [this](const httplib::Request& request, httplib::Response& response) {
bool ok = false;
for (auto& [keyStr, value] : request.params) {
u32 key = stringToKey(keyStr);
if (key != 0) {
bool state = (value == "1");
if (!state && value != "0") {
// Invalid state
ok = false;
break;
}
pushAction(HttpAction::createKeyAction(key, state));
ok = true;
} else {
// Invalid key
ok = false;
break;
}
}
response.set_content(ok ? "ok" : "error", "text/plain");
});
server->Get("/step", [this](const httplib::Request& request, httplib::Response& response) {
auto it = request.params.find("frames");
if (it == request.params.end()) {
response.set_content("error", "text/plain");
return;
}
int frames;
try {
frames = std::stoi(it->second);
} catch (...) {
response.set_content("error", "text/plain");
return;
}
if (frames <= 0) {
response.set_content("error", "text/plain");
return;
}
DeferredResponseWrapper wrapper(response);
std::unique_lock lock(wrapper.mutex);
pushAction(HttpAction::createStepAction(wrapper, frames));
wrapper.cv.wait(lock, [&wrapper] { return wrapper.ready; });
});
server->Get("/status", [this](const httplib::Request&, httplib::Response& response) { response.set_content(status(), "text/plain"); });
server->Get("/load_rom", [this](const httplib::Request& request, httplib::Response& response) {
auto it = request.params.find("path");
if (it == request.params.end()) {
response.set_content("error", "text/plain");
return;
}
std::filesystem::path romPath = it->second;
if (romPath.empty()) {
response.set_content("error", "text/plain");
return;
} else {
std::error_code error;
if (!std::filesystem::is_regular_file(romPath, error)) {
std::string message = "error: " + error.message();
response.set_content(message, "text/plain");
return;
}
}
bool paused = false;
it = request.params.find("paused");
if (it != request.params.end()) {
paused = (it->second == "1");
}
DeferredResponseWrapper wrapper(response);
std::unique_lock lock(wrapper.mutex);
pushAction(HttpAction::createLoadRomAction(wrapper, romPath, paused));
wrapper.cv.wait(lock, [&wrapper] { return wrapper.ready; });
});
server->Get("/togglepause", [this](const httplib::Request&, httplib::Response& response) {
pushAction(HttpAction::createTogglePauseAction());
response.set_content("ok", "text/plain");
});
server->Get("/reset", [this](const httplib::Request&, httplib::Response& response) {
pushAction(HttpAction::createResetAction());
response.set_content("ok", "text/plain");
});
// TODO: ability to specify host and port
printf("Starting HTTP server on port 1234\n");
server->listen("localhost", 1234);
}
std::string HttpServer::status() {
HIDService& hid = emulator->kernel.getServiceManager().getHID();
std::stringstream stringStream;
stringStream << "Panda3DS\n";
stringStream << "Status: " << (paused ? "Paused" : "Running") << "\n";
// TODO: This currently doesn't work for N3DS buttons
auto keyPressed = [](const HIDService& hid, u32 mask) { return (hid.getOldButtons() & mask) != 0; };
for (auto& [keyStr, value] : keyMap) {
stringStream << keyStr << ": " << keyPressed(hid, value) << "\n";
}
return stringStream.str();
}
void HttpServer::processActions() {
std::scoped_lock lock(actionQueueMutex);
if (framesToRun > 0) {
if (!currentStepAction) {
// Should never happen
printf("framesToRun > 0 but no currentStepAction\n");
return;
}
emulator->resume();
framesToRun--;
if (framesToRun == 0) {
paused = true;
emulator->pause();
DeferredResponseWrapper& response = reinterpret_cast<HttpActionStep*>(currentStepAction.get())->getResponse();
response.inner_response.set_content("ok", "text/plain");
std::unique_lock<std::mutex> lock(response.mutex);
response.ready = true;
response.cv.notify_one();
}
// Don't process more actions until we're done stepping
return;
}
HIDService& hid = emulator->kernel.getServiceManager().getHID();
while (!actionQueue.empty()) {
std::unique_ptr<HttpAction> action = std::move(actionQueue.front());
actionQueue.pop();
switch (action->getType()) {
case HttpActionType::Screenshot: {
HttpActionScreenshot* screenshotAction = static_cast<HttpActionScreenshot*>(action.get());
emulator->gpu.screenshot(httpServerScreenshotPath);
std::ifstream file(httpServerScreenshotPath, std::ios::binary);
std::vector<char> buffer(std::istreambuf_iterator<char>(file), {});
DeferredResponseWrapper& response = screenshotAction->getResponse();
response.inner_response.set_content(buffer.data(), buffer.size(), "image/png");
std::unique_lock<std::mutex> lock(response.mutex);
response.ready = true;
response.cv.notify_one();
break;
}
case HttpActionType::Key: {
HttpActionKey* keyAction = static_cast<HttpActionKey*>(action.get());
if (keyAction->getState()) {
hid.pressKey(keyAction->getKey());
} else {
hid.releaseKey(keyAction->getKey());
}
break;
}
case HttpActionType::LoadRom: {
HttpActionLoadRom* loadRomAction = static_cast<HttpActionLoadRom*>(action.get());
DeferredResponseWrapper& response = loadRomAction->getResponse();
bool loaded = emulator->loadROM(loadRomAction->getPath());
response.inner_response.set_content(loaded ? "ok" : "error", "text/plain");
std::unique_lock<std::mutex> lock(response.mutex);
response.ready = true;
response.cv.notify_one();
if (loaded) {
paused = loadRomAction->getPaused();
framesToRun = 0;
if (paused) {
emulator->pause();
} else {
emulator->resume();
}
}
break;
}
case HttpActionType::TogglePause:
framesToRun = 0;
emulator->togglePause();
paused = !paused;
break;
case HttpActionType::Reset: emulator->reset(Emulator::ReloadOption::Reload); break;
case HttpActionType::Step: {
HttpActionStep* stepAction = static_cast<HttpActionStep*>(action.get());
framesToRun = stepAction->getFrames();
currentStepAction = std::move(action);
break;
}
default: break;
}
}
}
u32 HttpServer::stringToKey(const std::string& key_name) {
if (keyMap.find(key_name) != keyMap.end()) {
return keyMap[key_name];
}
return 0;
}
#endif // PANDA3DS_ENABLE_HTTP_SERVER

View file

@ -1,132 +0,0 @@
#ifdef PANDA3DS_ENABLE_HTTP_SERVER
#include "httpserver.hpp"
#include <fstream>
#include <string>
#include <thread>
#include <vector>
#include <sstream>
#include "httplib.h"
#include "services/hid.hpp"
HttpServer::HttpServer() : keyMap(
{
{"A", { HID::Keys::A, false } },
{"B", { HID::Keys::B, false } },
{"Select", { HID::Keys::Select, false } },
{"Start", { HID::Keys::Start, false } },
{"Right", { HID::Keys::Right, false } },
{"Left", { HID::Keys::Left, false } },
{"Up", { HID::Keys::Up, false } },
{"Down", { HID::Keys::Down, false } },
{"R", { HID::Keys::R, false } },
{"L", { HID::Keys::L, false } },
{"X", { HID::Keys::X, false } },
{"Y", { HID::Keys::Y, false } },
}
) {}
void HttpServer::startHttpServer() {
std::thread http_thread([this]() {
httplib::Server server;
server.Get("/ping", [](const httplib::Request&, httplib::Response& response) { response.set_content("pong", "text/plain"); });
server.Get("/screen", [this](const httplib::Request&, httplib::Response& response) {
{
std::scoped_lock lock(actionMutex);
pendingAction = true;
action = HttpAction::Screenshot;
}
// wait until the screenshot is ready
pendingAction.wait(true);
std::ifstream image(httpServerScreenshotPath, std::ios::binary);
std::vector<char> buffer(std::istreambuf_iterator<char>(image), {});
response.set_content(buffer.data(), buffer.size(), "image/png");
});
server.Get("/input", [this](const httplib::Request& request, httplib::Response& response) {
bool ok = false;
for (auto& [keyStr, value] : request.params) {
auto key = stringToKey(keyStr);
printf("Param: %s\n", keyStr.c_str());
if (key != 0) {
std::scoped_lock lock(actionMutex);
pendingAction = true;
pendingKey = key;
ok = true;
if (value == "1") {
action = HttpAction::PressKey;
setKeyState(keyStr, true);
} else if (value == "0") {
action = HttpAction::ReleaseKey;
setKeyState(keyStr, false);
} else {
// Should not happen but just in case
pendingAction = false;
ok = false;
}
// Not supporting multiple keys at once for now (ever?)
break;
}
}
if (ok) {
response.set_content("ok", "text/plain");
}
});
server.Get("/step", [this](const httplib::Request&, httplib::Response& response) {
// TODO: implement /step
response.set_content("ok", "text/plain");
});
server.Get("/status", [this](const httplib::Request&, httplib::Response& response) {
response.set_content(status(), "text/plain");
});
// TODO: ability to specify host and port
printf("Starting HTTP server on port 1234\n");
server.listen("localhost", 1234);
});
http_thread.detach();
}
std::string HttpServer::status() {
std::stringstream stringStream;
stringStream << "Panda3DS\n";
stringStream << "Status: " << (paused ? "Paused" : "Running") << "\n";
for (auto& [keyStr, value] : keyMap) {
stringStream << keyStr << ": " << value.second << "\n";
}
return stringStream.str();
}
u32 HttpServer::stringToKey(const std::string& key_name) {
if (keyMap.find(key_name) != keyMap.end()) {
return keyMap[key_name].first;
}
return 0;
}
bool HttpServer::getKeyState(const std::string& key_name) {
if (keyMap.find(key_name) != keyMap.end()) {
return keyMap[key_name].second;
}
return false;
}
void HttpServer::setKeyState(const std::string& key_name, bool state) {
if (keyMap.find(key_name) != keyMap.end()) {
keyMap[key_name].second = state;
}
}
#endif // PANDA3DS_ENABLE_HTTP_SERVER

View file

@ -92,6 +92,12 @@ bool IOFile::seek(std::int64_t offset, int origin) {
return true;
}
bool IOFile::flush() {
if (!isOpen() || fflush(handle)) return false;
return true;
}
bool IOFile::rewind() { return seek(0, SEEK_SET); }
FILE* IOFile::getHandle() { return handle; }

View file

@ -1,9 +1,9 @@
#include "emulator.hpp"
int main (int argc, char *argv[]) {
Emulator emu;
int main(int argc, char *argv[]) {
Emulator emu;
emu.initGraphicsContext();
emu.initGraphicsContext();
if (argc > 1) {
auto romPath = std::filesystem::current_path() / argv[1];

View file

@ -1,4 +1,39 @@
#include "renderer.hpp"
Renderer::Renderer(GPU& gpu, const std::array<u32, regNum>& internalRegs) : gpu(gpu), regs(internalRegs) {}
Renderer::~Renderer() {}
#include <algorithm>
#include <unordered_map>
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) {
// Transform to lower-case to make the setting case-insensitive
std::transform(inString.begin(), inString.end(), inString.begin(), [](unsigned char c) { return std::tolower(c); });
// Huge table of possible names and misspellings
// Please stop misspelling Vulkan as Vulcan
static const std::unordered_map<std::string, RendererType> map = {
{"null", RendererType::Null}, {"nil", RendererType::Null}, {"none", RendererType::Null},
{"gl", RendererType::OpenGL}, {"ogl", RendererType::OpenGL}, {"opengl", RendererType::OpenGL},
{"vk", RendererType::Vulkan}, {"vulkan", RendererType::Vulkan}, {"vulcan", RendererType::Vulkan},
{"sw", RendererType::Software}, {"soft", RendererType::Software}, {"software", RendererType::Software},
{"softrast", RendererType::Software},
};
if (auto search = map.find(inString); search != map.end()) {
return search->second;
}
return std::nullopt;
}
const char* Renderer::typeToString(RendererType rendererType) {
switch (rendererType) {
case RendererType::Null: return "null";
case RendererType::OpenGL: return "opengl";
case RendererType::Vulkan: return "vulkan";
case RendererType::Software: return "software";
default: return "Invalid";
}
}