mirror of
https://github.com/wheremyfoodat/Panda3DS.git
synced 2025-06-16 15:55:24 +12:00
Merge branch 'master' into vfp-timing
This commit is contained in:
commit
639ef614eb
157 changed files with 6820 additions and 263696 deletions
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
235
src/core/action_replay.cpp
Normal 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
31
src/core/cheats.cpp
Normal 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!");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
76
src/core/fs/ivfc.cpp
Normal 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
194
src/core/fs/romfs.cpp
Normal 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
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
6
src/core/kernel/timers.cpp
Normal file
6
src/core/kernel/timers.cpp
Normal 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"); }
|
|
@ -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());
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
14
src/core/renderer_null/renderer_null.cpp
Normal file
14
src/core/renderer_null/renderer_null.cpp
Normal 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) {}
|
25
src/core/renderer_sw/renderer_sw.cpp
Normal file
25
src/core/renderer_sw/renderer_sw.cpp
Normal 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"); }
|
549
src/core/renderer_vk/renderer_vk.cpp
Normal file
549
src/core/renderer_vk/renderer_vk.cpp
Normal 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) {}
|
163
src/core/renderer_vk/vk_debug.cpp
Normal file
163
src/core/renderer_vk/vk_debug.cpp
Normal 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
|
3
src/core/renderer_vk/vulkan_api.cpp
Normal file
3
src/core/renderer_vk/vulkan_api.cpp
Normal file
|
@ -0,0 +1,3 @@
|
|||
#include "renderer_vk/vulkan_api.hpp"
|
||||
|
||||
VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE;
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
BIN
src/core/services/fonts/CitraSharedFontUSRelocated.bin
Normal file
BIN
src/core/services/fonts/CitraSharedFontUSRelocated.bin
Normal file
Binary file not shown.
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
42
src/core/services/http.cpp
Normal file
42
src/core/services/http.cpp
Normal 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);
|
||||
}
|
|
@ -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);
|
||||
}
|
27
src/core/services/mcu/mcu_hwc.cpp
Normal file
27
src/core/services/mcu/mcu_hwc.cpp
Normal 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);
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
15
src/core/services/news_u.cpp
Normal file
15
src/core/services/news_u.cpp
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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
33
src/core/services/soc.cpp
Normal 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
63
src/core/services/ssl.cpp
Normal 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);
|
||||
}
|
|
@ -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
41
src/discord_rpc.cpp
Normal 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
|
306
src/emulator.cpp
306
src/emulator.cpp
|
@ -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
|
8
src/host_shaders/opengl_display.frag
Normal file
8
src/host_shaders/opengl_display.frag
Normal 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);
|
||||
}
|
23
src/host_shaders/opengl_display.vert
Normal file
23
src/host_shaders/opengl_display.vert
Normal 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];
|
||||
}
|
417
src/host_shaders/opengl_fragment_shader.frag
Normal file
417
src/host_shaders/opengl_fragment_shader.frag
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
97
src/host_shaders/opengl_vertex_shader.vert
Normal file
97
src/host_shaders/opengl_vertex_shader.vert
Normal 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
356
src/http_server.cpp
Normal 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
|
|
@ -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
|
|
@ -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; }
|
||||
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue