mirror of
https://github.com/mikage-emu/mikage-dev.git
synced 2025-01-21 13:01:10 +01:00
Hello Mikage
This commit is contained in:
parent
6bb1b12a02
commit
c7a6ea0846
303 changed files with 78141 additions and 0 deletions
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "externals/teakra"]
|
||||
path = externals/teakra
|
||||
url = https://github.com/mikage-emu/teakra.git
|
82
CMakeLists.txt
Normal file
82
CMakeLists.txt
Normal file
|
@ -0,0 +1,82 @@
|
|||
# 3.11 added support for interface libraries
|
||||
# 3.13 detects and target-izes all of our boost dependencies properly
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
include(ExternalProject)
|
||||
|
||||
project(jmavu)
|
||||
|
||||
enable_testing()
|
||||
|
||||
if (NOT ANDROID)
|
||||
add_link_options("-fuse-ld=mold")
|
||||
endif()
|
||||
|
||||
option(USE_ASAN "Enable address sanitizer" OFF)
|
||||
if (USE_ASAN)
|
||||
add_compile_definitions(BOOST_USE_ASAN)
|
||||
add_compile_options(-fsanitize=address)
|
||||
add_link_options(-fsanitize=address)
|
||||
endif()
|
||||
|
||||
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${PROJECT_SOURCE_DIR}/externals/cmake")
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# TODO: Move these flags into a "boost" cmake target
|
||||
add_definitions(-DBOOST_FILESYSTEM_NO_DEPRECATED -DBOOST_COROUTINES_NO_DEPRECATION_WARNING)
|
||||
|
||||
#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -fstrict-aliasing -Wpedantic -Wfatal-errors -Wdocumentation -Weverything -Wextra -Wconversion -Wno-c++98-compat -Wno-c++98-c++11-compat-pedantic -Wno-c++98-compat-pedantic")
|
||||
# TODO: Consider -Wsuggest-override (since GCC 5.1)
|
||||
# TODO: When using clang, use -Wno-gnu-statement-expression: Necessary to prevent warning spam when using clang and the assert macro
|
||||
# TODO: When using clang, -Weverything
|
||||
#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -fstrict-aliasing -Wpedantic -Wfatal-errors -Wdocumentation")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -fstrict-aliasing -Wpedantic")
|
||||
|
||||
find_package(Boost 1.80.0 EXACT REQUIRED)
|
||||
|
||||
find_package(fmt REQUIRED)
|
||||
|
||||
find_package(range-v3 REQUIRED)
|
||||
|
||||
find_package(SDL2 REQUIRED)
|
||||
|
||||
find_package(spdlog REQUIRED)
|
||||
|
||||
find_package(cryptopp REQUIRED)
|
||||
|
||||
find_package(Pistache)
|
||||
if (Pistache_FOUND)
|
||||
add_compile_definitions(ENABLE_PISTACHE=1)
|
||||
else()
|
||||
add_compile_definitions(ENABLE_PISTACHE=0)
|
||||
endif()
|
||||
|
||||
find_package(Catch2 REQUIRED)
|
||||
|
||||
find_package(Tracy REQUIRED)
|
||||
|
||||
find_package(xxHash REQUIRED)
|
||||
|
||||
find_package(shaderc REQUIRED)
|
||||
|
||||
# TODO: These don't actually work for some reason :<
|
||||
#set_property(TARGET proj PROPERTY CXX_STANDARD 14)
|
||||
#set_property(TARGET proj PROPERTY CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
if(NOT ANDROID)
|
||||
find_package(libunwind)
|
||||
endif ()
|
||||
|
||||
# TODO: find_package-ify
|
||||
add_subdirectory(externals/teakra)
|
||||
set_target_properties(teakra_c PROPERTIES EXCLUDE_FROM_ALL 1)
|
||||
|
||||
add_subdirectory(source)
|
||||
add_subdirectory(data/nand_archives)
|
||||
if(NOT ANDROID)
|
||||
add_subdirectory(tools/3dsx_to_cia)
|
||||
#add_subdirectory(tools/embed_title)
|
||||
#add_subdirectory(tools/install_cia)
|
||||
endif()
|
55
README.md
Normal file
55
README.md
Normal file
|
@ -0,0 +1,55 @@
|
|||
# Mikage Developer Edition
|
||||
|
||||
## Build instructions
|
||||
|
||||
CMake and Conan 2 are required to build Mikage.
|
||||
* `mkdir build`
|
||||
* `cd build`
|
||||
* `conan install .. -of . --build=missing` (add `-s build_type=Debug` for Debug builds)
|
||||
* `cmake -DCMAKE_BUILD_TYPE=Release .. -G Ninja -DCMAKE_PREFIX_PATH=\`realpath .\``
|
||||
* `ninja`
|
||||
|
||||
The Conan install may fail if the shaderc package cannot be found. In this case,
|
||||
download our [custom recipe from conan-3ds](https://github.com/mikage-emu/conan-3ds/tree/mikage/packages/shaderc)
|
||||
and run `conan export . --version 2021.1` from within this `shaderc` folder.
|
||||
|
||||
## Usage
|
||||
|
||||
For the first launch, three things must be set up:
|
||||
1. a complete `aes_keys.txt` must be placed in the `build` folder
|
||||
2. a virtual NAND must be bootstrapped from a game update partition
|
||||
3. the 3DS initial system setup must be run
|
||||
|
||||
Given any game with an update partition, steps 2 and 3 can be done by running:
|
||||
```
|
||||
cd build && source conanrun.sh && ./source/mikage game.cci --bootstrap_nand --launch_menu
|
||||
```
|
||||
NOTE: Currently, this probably requires a game of the EU region. Patching
|
||||
`SecureInfo_A` by hand after bootstrapping may work, too.
|
||||
|
||||
Once set up, it's sufficient to run `./source/mikage game.cci` or `./source/mikage --launch_menu`.
|
||||
You'll need to `source conanrun.sh` to set up library directories.
|
||||
|
||||
## Key bindings
|
||||
|
||||
* Arrow →: A
|
||||
* Arrow ↓: B
|
||||
* Arrow ↑: X
|
||||
* Arrow ←: Y
|
||||
* WASD keys: Circle pad
|
||||
* Backspace: HOME (press twice to power down)
|
||||
* Q key: L
|
||||
* E key: L
|
||||
* IJKL keys: D-pad
|
||||
|
||||
## Debugging
|
||||
|
||||
Individual processes may be debugged through an embedded GDB remote server over
|
||||
network port 12345. To enable it, pass `--debug` and `--attach_to_process <pid>`
|
||||
when launching Mikage. After startup, connect via gdb using `tar ext :12345`,
|
||||
wait until Mikage prints a message about waiting for the debugger attach, then
|
||||
type `attach <PID>` in the gdb shell. Note that a gdb build for ARM targets may
|
||||
be necessary to do this. Debuggers other than gdb may work but are untested.
|
||||
|
||||
Processes running in the emulated kernel can be introspected through a simple
|
||||
debug console available via telnet on port 12347.
|
45
conanfile.py
Normal file
45
conanfile.py
Normal file
|
@ -0,0 +1,45 @@
|
|||
from conan import ConanFile
|
||||
|
||||
class MikageConan(ConanFile):
|
||||
name = "mikage"
|
||||
settings = "os", "compiler", "build_type", "arch"
|
||||
generators = "CMakeDeps"
|
||||
|
||||
requires = [
|
||||
#"boost/1.79.0",
|
||||
"boost/1.80.0",
|
||||
"spdlog/1.10.0",
|
||||
"cryptopp/8.5.0",
|
||||
"sdl/2.0.20", # 2.0.18 fixed swapped X/Y buttons on Switch Pro Controller
|
||||
"range-v3/0.11.0",
|
||||
"catch2/2.13.7",
|
||||
# NOTE: Later shaderc versions have drastically longer shader compile times
|
||||
"shaderc/2021.1",
|
||||
"tracy/0.9.1",
|
||||
"xxhash/0.8.0",
|
||||
"fmt/8.1.1",
|
||||
]
|
||||
|
||||
def configure(self):
|
||||
# TODO: Works around conan-center-index issue 7118
|
||||
self.options["sdl"].nas = False
|
||||
self.options["sdl"].alsa = False
|
||||
self.options["sdl"].shared = True
|
||||
self.options["pulseaudio"].shared = True
|
||||
self.options["pulseaudio"].with_alsa = False
|
||||
#self.options["sdl"].nas = True
|
||||
|
||||
self.options["tracy"].enable = False # TODO: Allow overriding this setting
|
||||
self.options["tracy"].fibers = True
|
||||
|
||||
if self.settings.os == "Android":
|
||||
# With zlib, libxml2 pulls in pkgconf, which Conan can't build for Android
|
||||
self.options["libxml2"].zlib = False
|
||||
|
||||
def requirements(self):
|
||||
# Pistache does not build on clang
|
||||
if self.settings.os != "Android" and self.settings.compiler != "clang":
|
||||
self.requires("pistache/cci.20201127")
|
||||
|
||||
if self.settings.os != "Android":
|
||||
self.requires("libunwind/1.8.0")
|
BIN
data/nand_archives/0004009b/00010202/content/00000000.app
Normal file
BIN
data/nand_archives/0004009b/00010202/content/00000000.app
Normal file
Binary file not shown.
BIN
data/nand_archives/0004009b/00010402/content/00000000.app
Normal file
BIN
data/nand_archives/0004009b/00010402/content/00000000.app
Normal file
Binary file not shown.
BIN
data/nand_archives/0004009b/00014002/content/00000000.app
Normal file
BIN
data/nand_archives/0004009b/00014002/content/00000000.app
Normal file
Binary file not shown.
BIN
data/nand_archives/000400db/00010302/content/00000000.app
Normal file
BIN
data/nand_archives/000400db/00010302/content/00000000.app
Normal file
Binary file not shown.
BIN
data/nand_archives/000400db/00016102/content/00000000.app
Normal file
BIN
data/nand_archives/000400db/00016102/content/00000000.app
Normal file
Binary file not shown.
16
data/nand_archives/CMakeLists.txt
Normal file
16
data/nand_archives/CMakeLists.txt
Normal file
|
@ -0,0 +1,16 @@
|
|||
# This directory contains pre-built NCCH archives that provide freely licensed
|
||||
# alternatives to system archives not included in game update partitions.
|
||||
#
|
||||
# The build scripts are located at https://github.com/mikage-emu/conan-3ds/tree/mikage/packages/citra_system_archives,
|
||||
# which is based on https://github.com/B3n30/citra_system_archives.
|
||||
|
||||
set(TITLES
|
||||
"0004009b/00010202"
|
||||
"0004009b/00010402"
|
||||
"0004009b/00014002"
|
||||
"000400db/00010302"
|
||||
"000400db/00016102")
|
||||
|
||||
foreach(TITLE ${TITLES})
|
||||
configure_file(${TITLE}/content/00000000.app ${CMAKE_BINARY_DIR}/data/${TITLE}/content/00000000.cxi COPYONLY)
|
||||
endforeach()
|
1
externals/teakra
vendored
Submodule
1
externals/teakra
vendored
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit ad99cb93dec7a042e3736d506783fb2dc1117851
|
97
source/CMakeLists.txt
Normal file
97
source/CMakeLists.txt
Normal file
|
@ -0,0 +1,97 @@
|
|||
add_subdirectory(framework)
|
||||
add_subdirectory(platform)
|
||||
add_subdirectory(video_core)
|
||||
|
||||
add_executable(mikage)
|
||||
target_sources(mikage PRIVATE
|
||||
display.cpp
|
||||
gdb_stub.cpp
|
||||
interpreter.cpp
|
||||
input.cpp
|
||||
ipc.cpp
|
||||
memory.cpp
|
||||
pica.cpp
|
||||
os.cpp
|
||||
os_console.cpp
|
||||
os_hypervisor.cpp
|
||||
os_serialization.cpp
|
||||
session.cpp
|
||||
settings.cpp
|
||||
arm/processor_default.cpp
|
||||
arm/thumb.cpp
|
||||
framework/console.cpp
|
||||
gui-sdl/main.cpp
|
||||
hardware/dsp.cpp
|
||||
loader/firm.cpp
|
||||
loader/gamecard.cpp
|
||||
loader/host_file.cpp
|
||||
processes/act.cpp
|
||||
processes/am.cpp
|
||||
processes/am_hpv.cpp
|
||||
processes/cam.cpp
|
||||
processes/cdc.cpp
|
||||
processes/cecd.cpp
|
||||
processes/csnd.cpp
|
||||
processes/dlp.cpp
|
||||
processes/dsp.cpp
|
||||
processes/dsp_hpv.cpp
|
||||
processes/dummy.cpp
|
||||
processes/errdisp.cpp
|
||||
processes/fake_process.cpp
|
||||
processes/friend.cpp
|
||||
processes/fs.cpp
|
||||
processes/fs_common.cpp
|
||||
processes/fs_hpv.cpp
|
||||
processes/gpio.cpp
|
||||
processes/hid.cpp
|
||||
processes/http.cpp
|
||||
processes/i2c.cpp
|
||||
processes/mcu.cpp
|
||||
processes/mic.cpp
|
||||
processes/ndm.cpp
|
||||
processes/ns.cpp
|
||||
processes/ns_hpv.cpp
|
||||
processes/news.cpp
|
||||
processes/nwm.cpp
|
||||
processes/pdn.cpp
|
||||
processes/ps.cpp
|
||||
processes/ptm.cpp
|
||||
processes/pxi.cpp
|
||||
processes/pxi_fs.cpp
|
||||
processes/pxi_fs_host_file.cpp
|
||||
processes/ro_hpv.cpp
|
||||
processes/sm_hpv.cpp
|
||||
processes/ssl.cpp
|
||||
ui/installer.cpp
|
||||
ui/key_database.cpp
|
||||
utility/simple_tcp.cpp)
|
||||
|
||||
if (Pistache_FOUND)
|
||||
target_sources(mikage PRIVATE
|
||||
debug_server.cpp
|
||||
debug/jit.cpp
|
||||
debug/os.cpp)
|
||||
endif()
|
||||
|
||||
target_include_directories(mikage PUBLIC .)
|
||||
target_link_libraries(mikage PRIVATE framework platform video_core)
|
||||
target_link_libraries(mikage PRIVATE vulkan-util)
|
||||
target_link_libraries(mikage PRIVATE teakra)
|
||||
target_link_libraries(mikage PRIVATE cryptopp::cryptopp)
|
||||
|
||||
# std::filesystem library must explicitly be linked on older stdlibc++ versions.
|
||||
# If needed, set this variable to stdc++fs or c++fs depending on the standard library used.
|
||||
if (STD_FILESYSTEM_LIBRARY)
|
||||
target_link_libraries(mikage PRIVATE ${STD_FILESYSTEM_LIBRARY})
|
||||
endif ()
|
||||
|
||||
target_link_libraries(mikage PRIVATE Boost::boost Boost::context Boost::filesystem Boost::iostreams Boost::program_options Boost::thread)
|
||||
|
||||
if (Pistache_FOUND)
|
||||
target_link_libraries(mikage PRIVATE Pistache::Pistache)
|
||||
endif()
|
||||
|
||||
target_link_libraries(mikage PRIVATE SDL2::SDL2)
|
||||
|
||||
target_link_libraries(mikage PRIVATE spdlog::spdlog)
|
||||
target_link_libraries(mikage PRIVATE Tracy::TracyClient)
|
588
source/arm.h
Normal file
588
source/arm.h
Normal file
|
@ -0,0 +1,588 @@
|
|||
#pragma once
|
||||
|
||||
#include "bit_field.h"
|
||||
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
|
||||
namespace ARM {
|
||||
|
||||
using BitField::v1::ViewBitField;
|
||||
using BitField::v1::BitField;
|
||||
|
||||
namespace Regs {
|
||||
enum {
|
||||
LR = 14,
|
||||
PC = 15,
|
||||
};
|
||||
}
|
||||
|
||||
enum class ProcessorMode {
|
||||
User = 0b10000,
|
||||
FIQ = 0b10001,
|
||||
IRQ = 0b10010,
|
||||
Supervisor = 0b10011,
|
||||
Abort = 0b10111,
|
||||
Undefined = 0b11011,
|
||||
System = 0b11111,
|
||||
};
|
||||
|
||||
// More convenient internal storage
|
||||
enum class InternalProcessorMode : uint32_t {
|
||||
User,
|
||||
FIQ,
|
||||
IRQ,
|
||||
Supervisor,
|
||||
Abort,
|
||||
Undefined,
|
||||
System,
|
||||
NumModes,
|
||||
Invalid = NumModes // Reserved for actually invalid (or "reserved") processor modes
|
||||
};
|
||||
static constexpr size_t NumInternalProcessorModes = static_cast<size_t>(InternalProcessorMode::NumModes);
|
||||
|
||||
inline InternalProcessorMode MakeInternal(ProcessorMode mode) {
|
||||
switch (mode) {
|
||||
case ProcessorMode::User: return InternalProcessorMode::User;
|
||||
case ProcessorMode::FIQ: return InternalProcessorMode::FIQ;
|
||||
case ProcessorMode::IRQ: return InternalProcessorMode::IRQ;
|
||||
case ProcessorMode::Supervisor: return InternalProcessorMode::Supervisor;
|
||||
case ProcessorMode::Abort: return InternalProcessorMode::Abort;
|
||||
case ProcessorMode::Undefined: return InternalProcessorMode::Undefined;
|
||||
case ProcessorMode::System: return InternalProcessorMode::System;
|
||||
default: return InternalProcessorMode::Invalid;
|
||||
}
|
||||
}
|
||||
|
||||
inline ProcessorMode MakeNative(InternalProcessorMode mode) {
|
||||
switch (mode) {
|
||||
case InternalProcessorMode::User: return ProcessorMode::User;
|
||||
case InternalProcessorMode::FIQ: return ProcessorMode::FIQ;
|
||||
case InternalProcessorMode::IRQ: return ProcessorMode::IRQ;
|
||||
case InternalProcessorMode::Supervisor: return ProcessorMode::Supervisor;
|
||||
case InternalProcessorMode::Abort: return ProcessorMode::Abort;
|
||||
case InternalProcessorMode::Undefined: return ProcessorMode::Undefined;
|
||||
case InternalProcessorMode::System: return ProcessorMode::System;
|
||||
default: throw std::runtime_error("Invalid internal processor mode");
|
||||
}
|
||||
}
|
||||
|
||||
enum class AddressingShift : uint32_t {
|
||||
LSL = 0,
|
||||
LSR = 1,
|
||||
ASR = 2,
|
||||
ROR_RRX = 3
|
||||
};
|
||||
|
||||
struct DoublePrecisionRegister {
|
||||
uint32_t raw_low;
|
||||
uint32_t raw_high;
|
||||
};
|
||||
|
||||
struct State {
|
||||
std::array<uint32_t,16> reg;
|
||||
|
||||
// VFP (floating point) registers
|
||||
// TODO: Current usage of this union is not compatible with strict aliasing rules!
|
||||
union {
|
||||
std::array<float, 32> fpreg;
|
||||
std::array<double, 16> dpreg;
|
||||
std::array<DoublePrecisionRegister, 16> dpreg_raw;
|
||||
};
|
||||
|
||||
// FP Status and Control Register
|
||||
union {
|
||||
uint32_t raw;
|
||||
|
||||
BitField<28, 1, uint32_t> unordered; // aka V
|
||||
BitField<29, 1, uint32_t> greater_equal_unordered; // aka C
|
||||
BitField<30, 1, uint32_t> equal; // aka Z
|
||||
BitField<31, 1, uint32_t> less; // aka N
|
||||
} fpscr;
|
||||
|
||||
// Current Program Status Register
|
||||
union ProgramStatusRegister {
|
||||
/**
|
||||
* Copies the given PSR into *this, as if a memcpy() had been performed.
|
||||
*/
|
||||
void RawCopyFrom(const ProgramStatusRegister& psr) {
|
||||
reinterpret_cast<uint32_t&>(*this) = reinterpret_cast<const uint32_t&>(psr);
|
||||
}
|
||||
|
||||
// Processor Mode ("M"): Guaranteed to be one of the values defined in #InternalProcessorMode
|
||||
BitField< 0, 5, InternalProcessorMode> mode; // "M"
|
||||
|
||||
// BitField< 0, 5, ProcessorMode> native_mode; // Used only for accessing within MSR. Do not use outside of that!
|
||||
|
||||
BitField< 5, 1, uint32_t> thumb; // "T"
|
||||
|
||||
BitField< 6, 1, uint32_t> F; // disable FIQ interrupts
|
||||
BitField< 7, 1, uint32_t> I; // disable IRQ interrupts
|
||||
BitField< 8, 1, uint32_t> A; // disable imprecise data aborts
|
||||
|
||||
BitField<16, 4, uint32_t> ge;
|
||||
|
||||
BitField<16, 2, uint32_t> ge10;
|
||||
BitField<18, 2, uint32_t> ge32;
|
||||
|
||||
BitField<16, 1, uint32_t> ge0;
|
||||
BitField<17, 1, uint32_t> ge1;
|
||||
BitField<18, 1, uint32_t> ge2;
|
||||
BitField<19, 1, uint32_t> ge3;
|
||||
|
||||
BitField<27, 1, uint32_t> q; // overflow or saturation in saturated integer arithmetic
|
||||
|
||||
BitField<28, 1, uint32_t> overflow; // "V"
|
||||
BitField<29, 1, uint32_t> carry; // "C"
|
||||
BitField<30, 1, uint32_t> zero; // "Z", "equal"
|
||||
BitField<31, 1, uint32_t> neg; // "N"
|
||||
|
||||
bool InPrivilegedMode() const {
|
||||
return (mode != InternalProcessorMode::User);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill PSR contents using the given value.
|
||||
* This is required to make sure the processor mode field is converted to InternalProcessorMode properly.
|
||||
*/
|
||||
static ProgramStatusRegister FromNativeRaw32(uint32_t val) {
|
||||
ProgramStatusRegister psr;
|
||||
reinterpret_cast<uint32_t&>(psr) = val;
|
||||
psr.mode = MakeInternal(ViewBitField<0, 5, ProcessorMode>(val));
|
||||
return psr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the raw uint32_t value expected by the ARM ISA.
|
||||
* This is required to make sure the processor mode field is converted to ProcessorMode properly.
|
||||
*/
|
||||
uint32_t ToNativeRaw32() const {
|
||||
uint32_t val = reinterpret_cast<const uint32_t&>(*this);
|
||||
ViewBitField<0, 5, ProcessorMode>(val).Assign(MakeNative(mode));
|
||||
return val;
|
||||
}
|
||||
};
|
||||
|
||||
ProgramStatusRegister cpsr;
|
||||
ProgramStatusRegister spsr[NumInternalProcessorModes]; // The SPSRs for User and System are just dummies!
|
||||
|
||||
bool InPrivilegedMode() const {
|
||||
return cpsr.InPrivilegedMode();
|
||||
}
|
||||
|
||||
bool HasSPSR() const {
|
||||
return (cpsr.mode != InternalProcessorMode::User && cpsr.mode != InternalProcessorMode::System);
|
||||
}
|
||||
|
||||
ProgramStatusRegister& GetSPSR(InternalProcessorMode mode) {
|
||||
return spsr[static_cast<uint32_t>(mode)];
|
||||
}
|
||||
|
||||
std::array<uint32_t,7> banked_regs_user; // r8-r14 (virtual bank used whenever another mode has its registers banked in; shared with System mode)
|
||||
std::array<uint32_t,8> banked_regs_fiq; // r8-r14 + SPSR_fiq
|
||||
std::array<uint32_t,3> banked_regs_svc; // r13 + r14 + SPSR_fiq
|
||||
std::array<uint32_t,3> banked_regs_abt; // r13 + r14 + SPSR_abt
|
||||
std::array<uint32_t,3> banked_regs_irq; // r13 + r14 + SPSR_irq
|
||||
std::array<uint32_t,3> banked_regs_und; // r13 + r14 + SPSR_und
|
||||
|
||||
/**
|
||||
* Move the given PSR to the CPSR register, performing a mode switch and other handling if necessary
|
||||
*/
|
||||
void ReplaceCPSR(ProgramStatusRegister psr) {
|
||||
if (cpsr.mode == psr.mode) {
|
||||
cpsr.RawCopyFrom(psr);
|
||||
return;
|
||||
}
|
||||
|
||||
// Bank out current registers
|
||||
switch (cpsr.mode) {
|
||||
case InternalProcessorMode::User:
|
||||
case InternalProcessorMode::System:
|
||||
std::copy(®[8], ®[15], banked_regs_user.begin());
|
||||
break;
|
||||
|
||||
case InternalProcessorMode::FIQ:
|
||||
std::copy(®[8], ®[15], banked_regs_fiq.begin());
|
||||
break;
|
||||
|
||||
case InternalProcessorMode::IRQ:
|
||||
std::copy(®[13], ®[15], banked_regs_irq.begin());
|
||||
break;
|
||||
|
||||
case InternalProcessorMode::Supervisor:
|
||||
std::copy(®[13], ®[15], banked_regs_svc.begin());
|
||||
break;
|
||||
|
||||
case InternalProcessorMode::Abort:
|
||||
std::copy(®[13], ®[15], banked_regs_abt.begin());
|
||||
break;
|
||||
|
||||
case InternalProcessorMode::Undefined:
|
||||
std::copy(®[13], ®[15], banked_regs_und.begin());
|
||||
break;
|
||||
|
||||
// It should not be possible to set cpsr.mode to this value
|
||||
case InternalProcessorMode::Invalid:
|
||||
assert(false);
|
||||
break;
|
||||
}
|
||||
|
||||
// Bank in new registers
|
||||
switch (cpsr.mode) {
|
||||
case InternalProcessorMode::User:
|
||||
case InternalProcessorMode::System:
|
||||
std::copy(banked_regs_user.begin(), banked_regs_user.end(), ®[8]);
|
||||
break;
|
||||
|
||||
case InternalProcessorMode::FIQ:
|
||||
std::copy(banked_regs_fiq.begin(), banked_regs_fiq.end(), ®[8]);
|
||||
break;
|
||||
|
||||
case InternalProcessorMode::IRQ:
|
||||
std::copy(banked_regs_irq.begin(), banked_regs_irq.end(), ®[13]);
|
||||
break;
|
||||
|
||||
case InternalProcessorMode::Supervisor:
|
||||
std::copy(banked_regs_svc.begin(), banked_regs_svc.end(), ®[13]);
|
||||
break;
|
||||
|
||||
case InternalProcessorMode::Abort:
|
||||
std::copy(banked_regs_abt.begin(), banked_regs_abt.end(), ®[13]);
|
||||
break;
|
||||
|
||||
case InternalProcessorMode::Undefined:
|
||||
std::copy(banked_regs_und.begin(), banked_regs_und.end(), ®[13]);
|
||||
break;
|
||||
|
||||
// It should not be possible to set cpsr.mode to this value
|
||||
case InternalProcessorMode::Invalid:
|
||||
assert(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t& LR() {
|
||||
return reg[14];
|
||||
}
|
||||
|
||||
uint32_t& PC() {
|
||||
return reg[15];
|
||||
}
|
||||
|
||||
uint32_t FetchReg(unsigned index) {
|
||||
if (index == 15)
|
||||
return PC() + 8;
|
||||
|
||||
return reg[index];
|
||||
}
|
||||
|
||||
union {
|
||||
// NOTE: The internal mapping from "raw" to any of the getter functions below is arbitrary.
|
||||
uint32_t raw[6];
|
||||
|
||||
auto& CPUId() {
|
||||
union Type {
|
||||
uint32_t raw;
|
||||
|
||||
BitField< 0, 4, uint32_t> CPUID;
|
||||
BitField< 4, 4, uint32_t> reserved1; // SBZ
|
||||
BitField< 8, 4, uint32_t> ClusterID;
|
||||
BitField<12, 20, uint32_t> reserved2; // SBZ
|
||||
};
|
||||
return reinterpret_cast<Type&>(raw[2]);
|
||||
}
|
||||
|
||||
auto& Control() {
|
||||
union Type {
|
||||
uint32_t raw;
|
||||
|
||||
BitField< 0, 1, uint32_t> M; // MMU enable
|
||||
BitField< 1, 1, uint32_t> A; // strict alignment fault checking enable
|
||||
BitField< 2, 1, uint32_t> C; // data cache enable
|
||||
BitField< 3, 4, uint32_t> reserved1; // Should Be One
|
||||
BitField< 7, 1, uint32_t> reserved2; // Should Be Zero
|
||||
BitField< 8, 1, uint32_t> S; // System protection (deprecated)
|
||||
BitField< 9, 1, uint32_t> R; // ROM protection (deprecated)
|
||||
BitField<10, 1, uint32_t> reserved3; // Should Be Zero
|
||||
BitField<11, 1, uint32_t> Z; // program flow prediction enable
|
||||
BitField<12, 1, uint32_t> I; // L1 instruction cache enable
|
||||
BitField<13, 1, uint32_t> V; // location of exception vectors: 0=normal(0x00000000), 1=high(0xFFFF0000)
|
||||
BitField<14, 1, uint32_t> reserved4; // Read As One/Should Be One
|
||||
BitField<15, 1, uint32_t> L4; // If zero, loads to PC will set the T bit (otherwise, they won't)
|
||||
BitField<16, 6, uint32_t> reserved5; // Read As 0b000101
|
||||
BitField<22, 1, uint32_t> U; // unaligned data access operation enable (including support for mixed-endianness data)
|
||||
BitField<23, 1, uint32_t> XP; // hardware page translation: subpage AP bits enable
|
||||
BitField<24, 1, uint32_t> reserved6; // SBZ/RAZ
|
||||
BitField<25, 1, uint32_t> EE; // Value is copied to the E bit of CPSR when taking an exception
|
||||
BitField<26, 1, uint32_t> reserved7; // SBZ/RAZ
|
||||
BitField<27, 1, uint32_t> NMFI; // If one, FIQs behave as NMFIs (otherwise, they have normal behavior)
|
||||
BitField<28, 1, uint32_t> TEX_remap; // TEX remap enable
|
||||
BitField<29, 1, uint32_t> force_AP; // Force AP enable
|
||||
BitField<30, 2, uint32_t> reserved8; // SBZ/RAZ
|
||||
};
|
||||
return reinterpret_cast<Type&>(raw[0]);
|
||||
}
|
||||
|
||||
auto& AuxiliaryControl() {
|
||||
union Type {
|
||||
uint32_t raw;
|
||||
|
||||
BitField<0, 1, uint32_t> RS; // Return stack enable (set on reset)
|
||||
BitField<1, 1, uint32_t> DB; // Dynamic branch prediction enable (set on reset)
|
||||
BitField<2, 1, uint32_t> SB; // Static branch prediction enable (set on reset)
|
||||
BitField<3, 1, uint32_t> F; // Instruction folding enable
|
||||
BitField<4, 1, uint32_t> EXCL; // 0 = L1 and L2 caches are inclusive, 1 = caches are exclusive
|
||||
BitField<5, 1, uint32_t> SMP; // 0 = AMP (no coherency), 1 = SMP (coherency)
|
||||
BitField<6, 1, uint32_t> L1_parity_checking; // L1 parity checking enable
|
||||
|
||||
BitField<7, 25, uint32_t> reserved; // Should be preserved
|
||||
};
|
||||
return reinterpret_cast<Type&>(raw[1]);
|
||||
}
|
||||
|
||||
// privileged modes, only
|
||||
auto& TranslationTableBase0() {
|
||||
union Type {
|
||||
uint32_t raw;
|
||||
|
||||
BitField< 3, 2, uint32_t> RGN; // Out cachable attributes for page table walking
|
||||
|
||||
BitField< 1, 1, uint32_t> S; // Page table walk to shared (S=1) or nonshared (S=0) memory
|
||||
|
||||
// Bits 13-ttbc.N:5 UNP/SBZ.
|
||||
|
||||
/*template<typename TTBC>
|
||||
uint32_t GetTTB0(const TTBC& ttbc) const {
|
||||
// return 18-N uppermost bits
|
||||
return BitFieldView<14, 18, uint32_t>(raw) >> ttbc.N;
|
||||
}*/
|
||||
};
|
||||
return reinterpret_cast<Type&>(raw[3]);
|
||||
}
|
||||
|
||||
// TODO: TranslationTableBase1
|
||||
|
||||
// privileged modes, only
|
||||
auto& TranslationTableBaseControl() {
|
||||
union Type {
|
||||
uint32_t raw;
|
||||
|
||||
// NOTE: Reading this register (or specifically, "N") returns the size of the page table boundary for TTBR0. SBZ_UNP should be zero then.
|
||||
// This is probably equivalent to just returning the value of N, though...
|
||||
|
||||
BitField< 0, 3, uint32_t> N; // 0=Use Translation Table Base Register 0; otherwise=Consider TTBR1
|
||||
BitField< 3, 29, uint32_t> SBZ_UNP; // Should Be Zero or Unpredictable
|
||||
};
|
||||
return reinterpret_cast<Type&>(raw[4]);
|
||||
}
|
||||
|
||||
// Accessible from all modes
|
||||
auto& ThreadLocalStorage() {
|
||||
union Type {
|
||||
// TODO: Double-check that this does not contain any other fields
|
||||
uint32_t virtual_addr;
|
||||
};
|
||||
return reinterpret_cast<Type&>(raw[5]);
|
||||
}
|
||||
} cp15;
|
||||
|
||||
uint64_t cycle_count;
|
||||
};
|
||||
static_assert(std::is_pod<State>::value, "State is not a POD type");
|
||||
|
||||
enum class OperandShifterMode : uint32_t {
|
||||
LSL = 0, // Logical Shift Left
|
||||
LSR = 1, // Logical Shift Right
|
||||
ASR = 2, // Arithmetic Shift Right
|
||||
ROR_RRX = 3, // ROtate Right or Rotate Right with Extend
|
||||
};
|
||||
|
||||
union ARMInstr {
|
||||
uint32_t raw;
|
||||
|
||||
BitField<28, 4, uint32_t> cond;
|
||||
|
||||
BitField<24, 4, uint32_t> opcode_prim;
|
||||
BitField< 4, 20, uint32_t> identifier_4_23;
|
||||
BitField<20, 8, uint32_t> identifier_20_27;
|
||||
|
||||
BitField< 0, 24, int32_t> branch_target;
|
||||
|
||||
BitField< 0, 8, uint32_t> immed_8;
|
||||
|
||||
BitField<22, 1, uint32_t> R;
|
||||
|
||||
BitField<25, 1, uint32_t> msr_I;
|
||||
BitField<16, 4, uint32_t> msr_field_mask;
|
||||
|
||||
uint32_t ExpandMSRFieldMask() const {
|
||||
return (msr_field_mask & 0x8) * (0xFF << 21)
|
||||
| (msr_field_mask & 0x4) * (0xFF << 14)
|
||||
| (msr_field_mask & 0x2) * (0xFF << 7)
|
||||
| (msr_field_mask & 0x1) * 0xFF;
|
||||
}
|
||||
|
||||
// Addressing Mode 1
|
||||
BitField< 5, 2, OperandShifterMode> addr1_shift;
|
||||
BitField< 7, 5, uint32_t> addr1_shift_imm;
|
||||
BitField< 8, 4, uint32_t> rotate_imm;
|
||||
BitField<20, 1, uint32_t> addr1_S; // update CPSR flags
|
||||
|
||||
BitField< 0, 4, uint32_t> addr3_immed_lo;
|
||||
BitField< 8, 4, uint32_t> addr3_immed_hi;
|
||||
|
||||
// The HLS bits are weird: Originally they were used to configure
|
||||
// signed/unsigned (S), load/store (L), and halfword/byte (H).
|
||||
// However, certain configurations are invalid or have a different meaning:
|
||||
// S=0, H=0 is a different instruction
|
||||
// S=1, L=0 is used to define double-word access
|
||||
BitField< 5, 1, uint32_t> addr3_H;
|
||||
BitField< 6, 1, uint32_t> addr3_S;
|
||||
BitField<20, 1, uint32_t> addr3_L;
|
||||
|
||||
// Addressing Mode 4 - Load/Store Multiple
|
||||
BitField< 0, 16, uint32_t> addr4_registers;
|
||||
BitField<20, 1, uint32_t> addr4_L; // 0=Store, 1=Load
|
||||
BitField<21, 1, uint32_t> addr4_W; // Update base register after the transfer
|
||||
BitField<22, 1, uint32_t> addr4_S; // ??
|
||||
BitField<23, 1, uint32_t> addr4_U; // 0=Decrement address, 1=Increment address
|
||||
BitField<24, 1, uint32_t> addr4_P; // 0=Change address after transfer, 1=Change address before transfer
|
||||
|
||||
// NOTE: Despite their name, these are generic to all adressing modes, it seems.
|
||||
// TODO: These are actually addressing mode 2/5 fields
|
||||
BitField<21, 1, uint32_t> ldr_W;
|
||||
BitField<23, 1, uint32_t> ldr_U;
|
||||
BitField<24, 1, uint32_t> ldr_P;
|
||||
BitField<25, 1, uint32_t> ldr_I;
|
||||
|
||||
// For VFP instructions, this bit is used to select a register index (D=0: bottom 16 registers, D=1: top 16 registers)
|
||||
BitField< 0, 8, uint32_t> addr5_offset; // actual offset is obtained by multiplying with 4
|
||||
|
||||
BitField<20, 1, uint32_t> addr5_L; // 0=Store, 1=Load
|
||||
BitField<22, 1, uint32_t> addr5_D;
|
||||
|
||||
BitField< 5, 2, OperandShifterMode> ldr_shift;
|
||||
BitField< 7, 5, uint32_t> ldr_shift_imm;
|
||||
BitField< 0, 12, uint32_t> ldr_offset;
|
||||
|
||||
BitField< 0, 4, uint32_t> idx_rm;
|
||||
BitField< 8, 4, uint32_t> idx_rs;
|
||||
BitField<12, 4, uint32_t> idx_rd;
|
||||
BitField<16, 4, uint32_t> idx_rn;
|
||||
|
||||
BitField<16, 5, uint32_t> sat_imm;
|
||||
|
||||
BitField< 6, 1, uint32_t> cps_F;
|
||||
BitField< 7, 1, uint32_t> cps_I;
|
||||
BitField< 8, 1, uint32_t> cps_A;
|
||||
BitField<19, 1, uint32_t> cps_imod_enable; // modify interrupt flags
|
||||
BitField<18, 1, uint32_t> cps_imod_value; // new value for interrupt flags (used iff cps_imod_enable is true)
|
||||
BitField<17, 1, uint32_t> cps_mmod; // modify processor mode
|
||||
BitField< 0, 5, ProcessorMode> cps_mode;
|
||||
|
||||
BitField< 8, 4, uint32_t> coproc_id;
|
||||
BitField<21, 3, uint32_t> coproc_opcode1;
|
||||
BitField< 5, 3, uint32_t> coproc_opcode2;
|
||||
|
||||
BitField<10, 2, uint32_t> uxtb_rotate;
|
||||
|
||||
BitField< 5, 1, uint32_t> vfp_data_M;
|
||||
BitField< 7, 1, uint32_t> vfp_data_N;
|
||||
|
||||
BitField< 6, 1, uint32_t> vfp_data_opcode_s;
|
||||
BitField<20, 1, uint32_t> vfp_data_opcode_r;
|
||||
BitField<21, 1, uint32_t> vfp_data_opcode_q;
|
||||
BitField<23, 1, uint32_t> vfp_data_opcode_p;
|
||||
};
|
||||
|
||||
enum class AddrMode1Encoding {
|
||||
Imm,
|
||||
ShiftByImm,
|
||||
ShiftByReg,
|
||||
Unknown
|
||||
};
|
||||
|
||||
inline AddrMode1Encoding GetAddrMode1Encoding(const ARMInstr& instr) {
|
||||
auto raw = instr.raw;
|
||||
|
||||
// Bits 26 and 27 must be zero
|
||||
bool is_valid_addr_mode1_instruction = (0 == ViewBitField<26, 2, uint32_t>(raw));
|
||||
if (!is_valid_addr_mode1_instruction)
|
||||
return AddrMode1Encoding::Unknown;
|
||||
|
||||
auto is_immediate = ViewBitField<25, 1, uint32_t>(raw);
|
||||
if (is_immediate)
|
||||
return AddrMode1Encoding::Imm;
|
||||
|
||||
// Check whether this is a register shift or an immediate shift
|
||||
auto is_register_shift = ViewBitField<4, 1, uint32_t>(raw);
|
||||
if (!is_register_shift)
|
||||
return AddrMode1Encoding::ShiftByImm;
|
||||
|
||||
// register shift encodings must have bit 7 set to zero to be valid.
|
||||
auto is_valid_register_shift = (0 == ViewBitField<7, 1, uint32_t>(raw));
|
||||
if (is_valid_register_shift)
|
||||
return AddrMode1Encoding::ShiftByReg;
|
||||
|
||||
return AddrMode1Encoding::Unknown;
|
||||
}
|
||||
|
||||
enum class AddrMode3AccessType {
|
||||
// These three load a small value into a larger register, hence sign-extension may be wished
|
||||
LoadSignedByte,
|
||||
LoadSignedHalfword,
|
||||
LoadUnsignedHalfword,
|
||||
|
||||
// These load values into equally-sized registers, hence sign-extension is not applicable
|
||||
LoadDoubleword,
|
||||
StoreByte,
|
||||
StoreHalfword,
|
||||
StoreDoubleword,
|
||||
|
||||
// Not an addressing mode 3 instruction
|
||||
Invalid
|
||||
};
|
||||
|
||||
union ThumbInstr {
|
||||
uint16_t raw;
|
||||
|
||||
BitField<13, 3, uint16_t> opcode_upper3;
|
||||
BitField<12, 4, uint16_t> opcode_upper4;
|
||||
BitField<11, 5, uint16_t> opcode_upper5;
|
||||
BitField<10, 6, uint16_t> opcode_upper6;
|
||||
BitField< 9, 7, uint16_t> opcode_upper7;
|
||||
BitField< 8, 8, uint16_t> opcode_upper8;
|
||||
BitField< 7, 9, uint16_t> opcode_upper9;
|
||||
BitField< 6, 10, uint16_t> opcode_upper10;
|
||||
|
||||
BitField<0, 3, uint16_t> idx_rd_low;
|
||||
BitField<3, 3, uint16_t> idx_rm;
|
||||
BitField<6, 3, uint16_t> idx_rn;
|
||||
BitField<8, 3, uint16_t> idx_rd_high;
|
||||
|
||||
BitField<6, 1, uint16_t> idx_rm_upperbit;
|
||||
BitField<7, 1, uint16_t> idx_rd_upperbit;
|
||||
|
||||
BitField<6, 5, uint16_t> immed_mid_5;
|
||||
BitField<6, 3, uint16_t> immed_mid_5_lower3;
|
||||
BitField<9, 2, uint16_t> immed_mid_5_upper2;
|
||||
|
||||
BitField<0, 7, uint16_t> immed_low_7;
|
||||
|
||||
BitField<0, 8, uint16_t> immed_low_8;
|
||||
BitField<0, 8, int16_t> signed_immed_low_8;
|
||||
|
||||
BitField<0, 11, int16_t> signed_immed_11;
|
||||
BitField<0, 11, uint16_t> unsigned_immed_11;
|
||||
|
||||
// Conditional Branch
|
||||
BitField<8, 4, uint16_t> cond;
|
||||
|
||||
// Push/Pop
|
||||
BitField<0, 8, uint16_t> register_list;
|
||||
};
|
||||
|
||||
} // namespace
|
334
source/arm/arm_meta.hpp
Normal file
334
source/arm/arm_meta.hpp
Normal file
|
@ -0,0 +1,334 @@
|
|||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
namespace ARM {
|
||||
|
||||
enum class Instr {
|
||||
AND,
|
||||
EOR,
|
||||
SUB,
|
||||
RSB,
|
||||
ADD,
|
||||
ADC,
|
||||
SBC,
|
||||
RSC,
|
||||
TST,
|
||||
TEQ,
|
||||
CMP,
|
||||
CMN,
|
||||
ORR,
|
||||
MOV,
|
||||
BIC,
|
||||
MVN,
|
||||
|
||||
MUL,
|
||||
|
||||
SSUB8,
|
||||
QSUB8,
|
||||
UADD8,
|
||||
USUB8,
|
||||
UQADD8,
|
||||
UQSUB8,
|
||||
UHADD8,
|
||||
SSAT,
|
||||
USAT,
|
||||
|
||||
SXTAH, // SXTAH and SXTH
|
||||
UXTB16,
|
||||
UXTAH, // UXTAH and UXTH
|
||||
|
||||
B,
|
||||
BL,
|
||||
BX,
|
||||
BLX,
|
||||
BLXImm,
|
||||
|
||||
LDR,
|
||||
LDRB,
|
||||
LDRH,
|
||||
LDRSH,
|
||||
LDRSB,
|
||||
LDRD,
|
||||
STR,
|
||||
STRB,
|
||||
STRH,
|
||||
STRD,
|
||||
|
||||
LDM,
|
||||
STM,
|
||||
|
||||
// Move (general-purpose) Register to PSR (Program Status Register)
|
||||
// Note that this encoding can be a nop by setting the field mask to all-zeroes
|
||||
MSR,
|
||||
|
||||
MRRC, // Move to ARM registers from Coprocessor
|
||||
MCRR, // Move to Coprocessor from ARM registers
|
||||
|
||||
// VFP instructions
|
||||
VLDR, // Load single register from memory
|
||||
VSTR, // Store single register to memory
|
||||
|
||||
VLDM, // Load multiple registers from memory
|
||||
VSTM, // Store multiple registers to memory
|
||||
|
||||
VFP_S, // VFP (single precision)
|
||||
VFP_D, // VFP (double precision)
|
||||
|
||||
MRRC_VFP, // Specialization for VFP coprocessor
|
||||
MCRR_VFP, // Specialization for VFP coprocessor
|
||||
|
||||
SWI, // Software Interrupt (i.e. system call)
|
||||
|
||||
Unknown
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
|
||||
constexpr std::array<char, 32> to_array(const char (&arr)[33]) {
|
||||
std::array<char, 32> ret { };
|
||||
for (std::size_t i = 0; i < ret.size(); ++i) {
|
||||
ret[i] = arr[i];
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
template<typename Rng, typename Value>
|
||||
constexpr auto find(Rng&& rng, Value&& value) {
|
||||
using std::begin;
|
||||
using std::end;
|
||||
auto it = begin(rng);
|
||||
auto endit = end(rng);
|
||||
for (; it != endit; ++it) {
|
||||
if (*it == value) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return it;
|
||||
}
|
||||
|
||||
template<typename Rng, typename Value>
|
||||
constexpr bool contains(Rng&& rng, Value&& value) {
|
||||
using std::end;
|
||||
return (find(std::forward<Rng>(rng), std::forward<Value>(value)) != end(rng));
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
struct InstrMetaInfo {
|
||||
constexpr InstrMetaInfo(const char (&arr)[33]) : bit_encoding(detail::to_array(arr)) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 32-character string specifying the semantics of each bit:
|
||||
* m, n, d, s: Rm, Rn, Rd, Rs
|
||||
* c: Conditional execution code
|
||||
* 'o', '/': Should be zero/one
|
||||
* A, B, C, D, E: Placeholders for fields specific to addressing modes 1, 2, 3, 4, and 5, respectively
|
||||
* P: Placeholder for coprocessor instructions
|
||||
* S (update conditional execution flags NCVZ)
|
||||
*/
|
||||
const std::array<char, 32> bit_encoding;
|
||||
};
|
||||
|
||||
inline constexpr std::pair<Instr, InstrMetaInfo> meta_table[] = {
|
||||
// Operations at the bottom have higher priority for dispatch
|
||||
|
||||
// UPPER .... LOWER
|
||||
{ Instr::AND, "cccc00A0000SnnnnddddAAAAAAAAAAAA" },
|
||||
{ Instr::EOR, "cccc00A0001SnnnnddddAAAAAAAAAAAA" },
|
||||
{ Instr::SUB, "cccc00A0010SnnnnddddAAAAAAAAAAAA" },
|
||||
{ Instr::RSB, "cccc00A0011SnnnnddddAAAAAAAAAAAA" },
|
||||
{ Instr::ADD, "cccc00A0100SnnnnddddAAAAAAAAAAAA" },
|
||||
{ Instr::ADC, "cccc00A0101SnnnnddddAAAAAAAAAAAA" },
|
||||
{ Instr::SBC, "cccc00A0110SnnnnddddAAAAAAAAAAAA" },
|
||||
{ Instr::RSC, "cccc00A0111SnnnnddddAAAAAAAAAAAA" },
|
||||
{ Instr::TST, "cccc00A10001nnnnooooAAAAAAAAAAAA" },
|
||||
{ Instr::TEQ, "cccc00A10011nnnnooooAAAAAAAAAAAA" },
|
||||
{ Instr::CMP, "cccc00A10101nnnnooooAAAAAAAAAAAA" },
|
||||
{ Instr::CMN, "cccc00A10111nnnnooooAAAAAAAAAAAA" },
|
||||
{ Instr::ORR, "cccc00A1100SnnnnddddAAAAAAAAAAAA" },
|
||||
{ Instr::MOV, "cccc00A1101SooooddddAAAAAAAAAAAA" },
|
||||
{ Instr::BIC, "cccc00A1110SnnnnddddAAAAAAAAAAAA" },
|
||||
{ Instr::MVN, "cccc00A1111SooooddddAAAAAAAAAAAA" },
|
||||
|
||||
// NOTE: Rn indeed is used as the destination register here
|
||||
{ Instr::MUL, "cccc0000000Sddddoooossss1001mmmm" },
|
||||
|
||||
{ Instr::SSUB8, "cccc01100001nnnndddd////1111mmmm" },
|
||||
{ Instr::QSUB8, "cccc01100010nnnndddd////1111mmmm" },
|
||||
{ Instr::UADD8, "cccc01100101nnnndddd////1001mmmm" },
|
||||
{ Instr::USUB8, "cccc01100101nnnndddd////1111mmmm" },
|
||||
{ Instr::UQADD8, "cccc01100110nnnndddd////1001mmmm" },
|
||||
{ Instr::UQSUB8, "cccc01100110nnnndddd////1111mmmm" },
|
||||
{ Instr::UHADD8, "cccc01100111nnnndddd////1001mmmm" },
|
||||
{ Instr::SSAT, "cccc0110101-----dddd------01mmmm" },
|
||||
{ Instr::USAT, "cccc0110111-----dddd------01mmmm" },
|
||||
|
||||
// NOTE: Also covers SXTH
|
||||
{ Instr::SXTAH, "cccc01101011nnnndddd--oo0111mmmm" },
|
||||
{ Instr::UXTB16, "cccc011011001111dddd--oo0111mmmm" },
|
||||
// NOTE: Also covers UXTH
|
||||
{ Instr::UXTAH, "cccc01101111nnnndddd--oo0111mmmm" },
|
||||
|
||||
{ Instr::B, "cccc1010------------------------" },
|
||||
{ Instr::BL, "cccc1011------------------------" },
|
||||
{ Instr::BX, "cccc00010010////////////0001mmmm" },
|
||||
{ Instr::BLX, "cccc00010010////////////0011mmmm" }, // Register variant
|
||||
{ Instr::BLXImm, "1111101-------------------------" }, // Immediate variant
|
||||
|
||||
{ Instr::LDR, "cccc01BBB0B1nnnnddddBBBBBBBBBBBB" },
|
||||
{ Instr::STR, "cccc01BBB0B0nnnnddddBBBBBBBBBBBB" },
|
||||
{ Instr::LDRB, "cccc01BBB1B1nnnnddddBBBBBBBBBBBB" },
|
||||
{ Instr::STRB, "cccc01BBB1B0nnnnddddBBBBBBBBBBBB" },
|
||||
|
||||
// L SH
|
||||
{ Instr::STRH, "cccc000CCCC0nnnnddddCCCC1011CCCC" },
|
||||
{ Instr::LDRD, "cccc000CCCC0nnnnddddCCCC1101CCCC" },
|
||||
{ Instr::STRD, "cccc000CCCC0nnnnddddCCCC1111CCCC" },
|
||||
{ Instr::LDRH, "cccc000CCCC1nnnnddddCCCC1011CCCC" },
|
||||
{ Instr::LDRSB, "cccc000CCCC1nnnnddddCCCC1101CCCC" },
|
||||
{ Instr::LDRSH, "cccc000CCCC1nnnnddddCCCC1111CCCC" },
|
||||
|
||||
{ Instr::LDM, "cccc100DDDD1nnnnDDDDDDDDDDDDDDDD" },
|
||||
{ Instr::STM, "cccc100DDDD0nnnnDDDDDDDDDDDDDDDD" },
|
||||
|
||||
{ Instr::MSR, "cccc00110-10----////------------"}, // Immediate variant
|
||||
{ Instr::MSR, "cccc00010-10----////oooo0000mmmm"}, // Register variant
|
||||
|
||||
{ Instr::MRRC, "cccc11000101nnnnddddPPPPPPPPmmmm" },
|
||||
{ Instr::MCRR, "cccc11000100nnnnddddPPPPPPPPmmmm" },
|
||||
|
||||
{ Instr::SWI, "cccc1111------------------------" },
|
||||
|
||||
// Coprocessor instructions (bit 8 indicates single/double precision)
|
||||
{ Instr::VLDM, "cccc110PPPP1nnnndddd101PEEEEEEEE" },
|
||||
{ Instr::VSTM, "cccc110PPPP0nnnndddd101PEEEEEEEE" },
|
||||
|
||||
{ Instr::MRRC_VFP, "cccc11000101nnnndddd101P00P1mmmm" },
|
||||
{ Instr::MCRR_VFP, "cccc11000100nnnndddd101P00P1mmmm" },
|
||||
|
||||
{ Instr::VLDR, "cccc1101EE01nnnndddd101PPPPPPPPP" },
|
||||
{ Instr::VSTR, "cccc1101EE00nnnndddd101PPPPPPPPP" },
|
||||
|
||||
{ Instr::VFP_S, "cccc1110PdPPnnnndddd1010nPm0mmmm" },
|
||||
// TODO: Most variants of this only allow upper n/d/m to be 0!
|
||||
{ Instr::VFP_D, "cccc1110PdPPnnnndddd1011nPm0mmmm" },
|
||||
};
|
||||
|
||||
template<typename F>
|
||||
constexpr std::array<std::invoke_result_t<F, Instr>, 8192> GenerateDispatchTable(F&& f, std::invoke_result_t<F, Instr> default_value = { }) {
|
||||
std::array<std::invoke_result_t<F, Instr>, 8192> table { };
|
||||
for (auto& entry : table) {
|
||||
entry = default_value;
|
||||
}
|
||||
|
||||
for (auto& instr : meta_table) {
|
||||
const bool is_addressing_mode_1 = detail::contains(instr.second.bit_encoding, 'A');
|
||||
const bool is_addressing_mode_2 = detail::contains(instr.second.bit_encoding, 'B');
|
||||
const bool is_addressing_mode_5 = detail::contains(instr.second.bit_encoding, 'E');
|
||||
const bool is_coprocessor_op = is_addressing_mode_5 || detail::contains(instr.second.bit_encoding, 'P');
|
||||
|
||||
uint32_t output_key = 0;
|
||||
uint32_t entropy = 0; // Number of non-fixed bits (i.e. logarithmic entropy)
|
||||
uint32_t dynamic_bits[12] { };
|
||||
|
||||
for (uint32_t key_bit = 0; key_bit < 12; ++key_bit) {
|
||||
// Encoding bits 27-20 and 7-4 (in that order)
|
||||
uint32_t index_bits[] = { 27u, 26u, 25u, 24u, 11u, 10u, 9u, 8u, 7u, 6u, 5u, 4u };
|
||||
if (is_coprocessor_op) {
|
||||
// For coprocessor (addressing mode 5) instructions, use bits 11-8 instead of 7-4
|
||||
index_bits[0] = 23;
|
||||
index_bits[1] = 22;
|
||||
index_bits[2] = 21;
|
||||
index_bits[3] = 20;
|
||||
}
|
||||
switch (instr.second.bit_encoding[index_bits[key_bit]]) {
|
||||
// Fixed bit: Always 0
|
||||
case '0':
|
||||
case 'o':
|
||||
break;
|
||||
|
||||
// Fixed bit: Always 1
|
||||
case '1':
|
||||
case '/':
|
||||
output_key |= (uint32_t { 1 } << key_bit);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Dynamic bit:
|
||||
// Either 0 or 1; possible encodings are enumerated below.
|
||||
// Instruction-specific encoding constraints are enforced later
|
||||
dynamic_bits[entropy] = key_bit;
|
||||
++entropy;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto entry = f(instr.first);
|
||||
// With N dynamic bits, there are up to N different encodings that match the instruction pattern. Let's iterate over all of them:
|
||||
for (uint32_t encoding_index = 0; encoding_index < (1u << entropy); ++encoding_index) {
|
||||
// Translate the index of the encoding to the key in the dispatch table
|
||||
uint32_t dynamic_key = output_key;
|
||||
for (uint32_t dynamic_bit_index = 0; dynamic_bit_index < entropy; ++dynamic_bit_index) {
|
||||
if (encoding_index & (1 << dynamic_bit_index)) {
|
||||
dynamic_key |= (1 << dynamic_bits[dynamic_bit_index]);
|
||||
}
|
||||
}
|
||||
|
||||
// Enforce instruction-specific constraints:
|
||||
if (is_addressing_mode_1) {
|
||||
// If bit 4 and 7 in the instruction are set and bit 25 is unset, this is not an addressing mode 1 instruction
|
||||
if (dynamic_key == (dynamic_key | 0b1001) && ((dynamic_key >> 9) & 1) == 0) {
|
||||
// Discard this encoding
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_addressing_mode_2) {
|
||||
// If bit 4 and 25 in the instruction are set, this is not an addressing mode 2 instruction
|
||||
if (dynamic_key == (dynamic_key | 0b1) && ((dynamic_key >> 9) & 1) == 1) {
|
||||
// Discard this encoding
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: For addressing mode 3, (!instr.ldr_P && instr.ldr_W) should filter out the key!
|
||||
|
||||
if (is_addressing_mode_5 && (instr.first == Instr::VLDM || instr.first == Instr::VSTM || instr.first == Instr::VLDR || instr.first == Instr::VSTR)) {
|
||||
// If bit 21 (W) is set, bit 23 (U) and bit 24 (P) must be different
|
||||
// (otherwise this is instruction is UNDEFINED)
|
||||
const bool w = ((dynamic_key >> 5) & 1);
|
||||
const bool u = ((dynamic_key >> 7) & 1);
|
||||
const bool p = ((dynamic_key >> 8) & 1);
|
||||
if (w && (u == p)) {
|
||||
// Discard this encoding
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (instr.second.bit_encoding[0] == '1' &&
|
||||
instr.second.bit_encoding[1] == '1' &&
|
||||
instr.second.bit_encoding[2] == '1' &&
|
||||
instr.second.bit_encoding[3] == '1') {
|
||||
// Distinguish special unconditional operations in the table key
|
||||
dynamic_key |= (1 << 12);
|
||||
}
|
||||
|
||||
table[dynamic_key] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
constexpr uint32_t BuildDispatchTableKey(uint32_t arm_instr) {
|
||||
// For coprocessor instructions, use bits 8-11 instead of 4-7
|
||||
const bool is_coprocessor = (((arm_instr >> 25) & 0x7) == 0b110 || ((arm_instr >> 24) & 0xf) == 0b1110);
|
||||
uint32_t lower_key = (is_coprocessor ? ((arm_instr & 0xf00) >> 8) : ((arm_instr & 0xf0) >> 4));
|
||||
return ((uint32_t { (arm_instr >> 28) == 0xf } << 12) | ((arm_instr & 0xff00000) >> 16) | lower_key);
|
||||
}
|
||||
|
||||
} // namespace ARM
|
108
source/arm/processor_default.cpp
Normal file
108
source/arm/processor_default.cpp
Normal file
|
@ -0,0 +1,108 @@
|
|||
#include "processor_default.hpp"
|
||||
|
||||
#include <range/v3/algorithm/fill.hpp>
|
||||
|
||||
namespace Interpreter {
|
||||
|
||||
PageTable::PageTable() noexcept {
|
||||
ranges::fill(physical_addresses, 0xffffffff);
|
||||
}
|
||||
|
||||
void PageTable::Insert(Memory::PhysicalMemory& mem, uint32_t vstart, uint32_t pstart, uint32_t size) {
|
||||
auto first_page_index = (vstart >> 12);
|
||||
auto last_page_index = ((vstart + size - 1) >> 12); // Inclusive
|
||||
for (auto page_offset = 0; page_offset <= last_page_index - first_page_index; ++page_offset) {
|
||||
const auto physical_address = pstart + page_offset * 0x1000;
|
||||
physical_addresses[first_page_index + page_offset] = physical_address;
|
||||
virtual_addresses_for[physical_address >> 12].push_back((first_page_index + page_offset) << 12);
|
||||
if (auto memory = Memory::LookupMemoryBackedPage(mem, physical_address)) {
|
||||
host_memory[first_page_index + page_offset] = memory;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PageTable::Remove(uint32_t vstart, uint32_t size) {
|
||||
// TODO: Assert vstart + size doesn't overflow the 32-bit range...
|
||||
|
||||
{
|
||||
auto begin_it = &physical_addresses[vstart >> 12];
|
||||
auto end_it = &physical_addresses[(vstart + size) >> 12];
|
||||
std::transform(begin_it, end_it, begin_it,
|
||||
[=](auto paddr) {
|
||||
if (paddr == 0xffffffff) {
|
||||
throw std::runtime_error("Couldn't find virtual memory mapping for removal");
|
||||
}
|
||||
|
||||
virtual_addresses_for[paddr >> 12].clear();
|
||||
|
||||
return 0xffffffff;
|
||||
});
|
||||
}
|
||||
|
||||
// Remove host memory backed pages, if any
|
||||
{
|
||||
auto begin_it = &host_memory[vstart >> 12];
|
||||
auto end_it = &host_memory[(vstart + size) >> 12];
|
||||
std::fill(begin_it, end_it, Memory::HostMemoryBackedPage { nullptr });
|
||||
}
|
||||
}
|
||||
|
||||
ProcessorWithDefaultMemory::ProcessorWithDefaultMemory(Interpreter::Setup& setup)
|
||||
: Memory::PhysicalMemorySubscriber(setup.mem), setup(setup) {
|
||||
// TODO: Initialize page_table based on current Memory state!
|
||||
}
|
||||
|
||||
void ProcessorWithDefaultMemory::WriteVirtualMemory8(uint32_t virt_address, const uint8_t value) {
|
||||
WriteVirtualMemory(setup.mem, page_table, virt_address, value);
|
||||
}
|
||||
|
||||
void ProcessorWithDefaultMemory::WriteVirtualMemory16(uint32_t virt_address, const uint16_t value) {
|
||||
WriteVirtualMemory(setup.mem, page_table, virt_address, value);
|
||||
}
|
||||
|
||||
void ProcessorWithDefaultMemory::WriteVirtualMemory32(uint32_t virt_address, const uint32_t value) {
|
||||
WriteVirtualMemory(setup.mem, page_table, virt_address, value);
|
||||
}
|
||||
|
||||
uint8_t ProcessorWithDefaultMemory::ReadVirtualMemory8(uint32_t virt_address) {
|
||||
return ReadVirtualMemory<uint8_t>(setup.mem, page_table, virt_address);
|
||||
}
|
||||
|
||||
uint16_t ProcessorWithDefaultMemory::ReadVirtualMemory16(uint32_t virt_address) {
|
||||
return ReadVirtualMemory<uint16_t>(setup.mem, page_table, virt_address);
|
||||
}
|
||||
|
||||
uint32_t ProcessorWithDefaultMemory::ReadVirtualMemory32(uint32_t virt_address) {
|
||||
return ReadVirtualMemory<uint32_t>(setup.mem, page_table, virt_address);
|
||||
}
|
||||
|
||||
void ProcessorWithDefaultMemory::OnVirtualMemoryMapped(uint32_t phys_addr, uint32_t size, uint32_t vaddr) {
|
||||
page_table.Insert(setup.mem, vaddr, phys_addr, size);
|
||||
}
|
||||
|
||||
void ProcessorWithDefaultMemory::OnVirtualMemoryUnmapped(uint32_t vaddr, uint32_t size) {
|
||||
page_table.Remove(vaddr, size);
|
||||
}
|
||||
|
||||
void ProcessorWithDefaultMemory::OnBackedByHostMemory(Memory::HostMemoryBackedPage page, uint32_t physical_address) {
|
||||
for (auto& virtual_address : page_table.virtual_addresses_for[physical_address >> 12]) {
|
||||
auto& page_table_entry = page_table.host_memory[virtual_address >> 12];
|
||||
if (page_table_entry) {
|
||||
// TODO: When creating new processes, we need to initialize page_table based on the current memory state
|
||||
// throw std::runtime_error(fmt::format("Attempted to override page table cache entry at address {:#x} that already existed", physical_address));
|
||||
}
|
||||
page_table_entry = page;
|
||||
}
|
||||
}
|
||||
|
||||
void ProcessorWithDefaultMemory::OnUnbackedByHostMemory(uint32_t physical_address) {
|
||||
for (auto& virtual_address : page_table.virtual_addresses_for[physical_address >> 12]) {
|
||||
auto& page_table_entry = page_table.host_memory[virtual_address >> 12];
|
||||
if (!page_table_entry) {
|
||||
throw std::runtime_error("Attempted to remove page table cache entry that does not exist");
|
||||
}
|
||||
page_table_entry = { };
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Interpreter
|
158
source/arm/processor_default.hpp
Normal file
158
source/arm/processor_default.hpp
Normal file
|
@ -0,0 +1,158 @@
|
|||
#pragma once
|
||||
|
||||
#include <interpreter.h>
|
||||
|
||||
#include <boost/container/small_vector.hpp>
|
||||
|
||||
namespace Interpreter {
|
||||
|
||||
class ExecutionContextWithDefaultMemory;
|
||||
|
||||
/**
|
||||
* Interface to access memory using virtual addresses.
|
||||
*
|
||||
* Exposes HostMemoryBackedPages that can directly be written to, and a
|
||||
* direct mapping from virtual addresses to physical ones to be used for slow
|
||||
* fallback paths in case the memory is not host memory backed.
|
||||
*/
|
||||
struct PageTable {
|
||||
PageTable() noexcept;
|
||||
|
||||
void Insert(Memory::PhysicalMemory& mem, uint32_t vstart, uint32_t pstart, uint32_t size);
|
||||
|
||||
void Remove(uint32_t vstart, uint32_t size);
|
||||
|
||||
Memory::HostMemoryBackedPage LookupHostMemory(uint32_t vaddr) const {
|
||||
return host_memory[vaddr >> 12];
|
||||
}
|
||||
|
||||
static constexpr uint32_t num_pages = (1 << 20);
|
||||
|
||||
// List mapping virtual memory page indexes to their corresponding physical memory page
|
||||
// (one entry per page in the entire 32-bit address space)
|
||||
std::array<Memory::HostMemoryBackedPage, num_pages> host_memory;
|
||||
|
||||
// List mapping virtual memory page indexes to their corresponding physical memory address
|
||||
std::array</*PAddr*/ uint32_t, num_pages> physical_addresses; // 0xffffffff signalizes an unmapped address
|
||||
|
||||
/**
|
||||
* List mapping physical memory page indexes to a list of virtual memory addresses that are mapped to them
|
||||
* Typically, for each 3DS process there are at most 2 (usually just 1) virtual pages that map to the same physical page
|
||||
*/
|
||||
std::array<boost::container::small_vector</*VAddr*/ uint32_t, 2>, num_pages> virtual_addresses_for;
|
||||
};
|
||||
|
||||
class ProcessorWithDefaultMemory : public Processor, Memory::PhysicalMemorySubscriber {
|
||||
protected:
|
||||
Setup& setup;
|
||||
PageTable page_table;
|
||||
|
||||
ProcessorWithDefaultMemory(Interpreter::Setup& setup);
|
||||
|
||||
friend class ExecutionContextWithDefaultMemory;
|
||||
|
||||
static std::optional<uint32_t> TranslateVirtualAddress(const PageTable& page_table, uint32_t vaddr) {
|
||||
auto paddr_start = page_table.physical_addresses[vaddr >> 12];
|
||||
|
||||
if (paddr_start == 0xffffffff) {
|
||||
throw std::runtime_error("Virtual address " + fmt::format("{:#x}", vaddr) + " is not mapped");
|
||||
}
|
||||
|
||||
return paddr_start + (vaddr & 0xfff);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static void WriteVirtualMemory(Memory::PhysicalMemory& mem, const PageTable& page_table, uint32_t address, T value) {
|
||||
auto page = page_table.LookupHostMemory(address);
|
||||
if (page) {
|
||||
Memory::Write(page, address & 0xfff, value);
|
||||
return;
|
||||
}
|
||||
// Else fall back to slow handler-based write
|
||||
|
||||
auto opt_paddr = TranslateVirtualAddress(page_table, address);
|
||||
if (!opt_paddr) {
|
||||
throw std::runtime_error(fmt::format("Invalid virtual address {:#x}", address));
|
||||
}
|
||||
|
||||
Memory::WriteLegacy(mem, *opt_paddr, value);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static T ReadVirtualMemory(Memory::PhysicalMemory& mem, const PageTable& page_table, uint32_t address) {
|
||||
auto page = page_table.LookupHostMemory(address);
|
||||
if (page) {
|
||||
return Memory::Read<T>(page, address & 0xfff);
|
||||
}
|
||||
// Else fall back to slow handler-based read
|
||||
|
||||
auto opt_paddr = TranslateVirtualAddress(page_table, address);
|
||||
if (!opt_paddr) {
|
||||
throw std::runtime_error(fmt::format("Invalid virtual address {:#x}", address));
|
||||
}
|
||||
|
||||
return Memory::ReadLegacy<T>(mem, *opt_paddr);
|
||||
}
|
||||
|
||||
// Narrow interface to the more refined return type ExecutionContextWithDefaultMemory
|
||||
ExecutionContext* CreateExecutionContextImpl() final;
|
||||
virtual ExecutionContextWithDefaultMemory* CreateExecutionContextImpl2() = 0;
|
||||
|
||||
void OnBackedByHostMemory(Memory::HostMemoryBackedPage, uint32_t address) override;
|
||||
void OnUnbackedByHostMemory(uint32_t address) override;
|
||||
|
||||
public:
|
||||
Setup& GetSetup() {
|
||||
return setup;
|
||||
}
|
||||
|
||||
void WriteVirtualMemory8(uint32_t virt_address, const uint8_t value) override;
|
||||
void WriteVirtualMemory16(uint32_t virt_address, const uint16_t value) override;
|
||||
void WriteVirtualMemory32(uint32_t virt_address, const uint32_t value) override;
|
||||
|
||||
uint8_t ReadVirtualMemory8(uint32_t virt_address) override;
|
||||
uint16_t ReadVirtualMemory16(uint32_t virt_address) override;
|
||||
uint32_t ReadVirtualMemory32(uint32_t virt_address) override;
|
||||
|
||||
void OnVirtualMemoryMapped(uint32_t phys_addr, uint32_t size, uint32_t vaddr) override;
|
||||
|
||||
void OnVirtualMemoryUnmapped(uint32_t vaddr, uint32_t size) override;
|
||||
};
|
||||
|
||||
class ExecutionContextWithDefaultMemory : public ExecutionContext {
|
||||
Memory::PhysicalMemory& mem;
|
||||
|
||||
const PageTable& page_table;
|
||||
|
||||
friend class ProcessorWithDefaultMemory;
|
||||
|
||||
protected:
|
||||
ExecutionContextWithDefaultMemory(Processor& parent_, Memory::PhysicalMemory& mem_)
|
||||
: ExecutionContext(parent_), mem(mem_), page_table(static_cast<ProcessorWithDefaultMemory&>(parent).page_table) {
|
||||
}
|
||||
|
||||
std::optional<uint32_t> TranslateVirtualAddress(uint32_t vaddr) {
|
||||
return ProcessorWithDefaultMemory::TranslateVirtualAddress(page_table, vaddr);
|
||||
}
|
||||
|
||||
public:
|
||||
// Re-define memory accessors here to get better codegen:
|
||||
// The JIT engines must be able to emit calls to memory read handlers
|
||||
// without them being wrapped in unnecessary layers of function calls.
|
||||
|
||||
template<typename T>
|
||||
void WriteVirtualMemory(uint32_t address, T value) {
|
||||
ProcessorWithDefaultMemory::WriteVirtualMemory<T>(mem, page_table, address, value);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T ReadVirtualMemory(uint32_t address) {
|
||||
return ProcessorWithDefaultMemory::ReadVirtualMemory<T>(mem, page_table, address);
|
||||
}
|
||||
};
|
||||
|
||||
inline ExecutionContext* ProcessorWithDefaultMemory::CreateExecutionContextImpl() {
|
||||
return CreateExecutionContextImpl2();
|
||||
}
|
||||
|
||||
} // namespace Interpreter
|
48
source/arm/processor_interpreter.hpp
Normal file
48
source/arm/processor_interpreter.hpp
Normal file
|
@ -0,0 +1,48 @@
|
|||
#pragma once
|
||||
|
||||
#include "processor_default.hpp"
|
||||
|
||||
#include <boost/coroutine2/coroutine.hpp>
|
||||
|
||||
namespace Interpreter {
|
||||
|
||||
// TODO: Instead of inhering CPUContext, integrate CPUContext into this class!
|
||||
struct InterpreterExecutionContext : ExecutionContextWithDefaultMemory, CPUContext {
|
||||
InterpreterExecutionContext(Processor& parent_, Setup& setup)
|
||||
: ExecutionContextWithDefaultMemory(parent_, setup.mem), CPUContext(setup.os.get(), &setup) {
|
||||
|
||||
// TODO: Setup virtual memory mappings
|
||||
}
|
||||
|
||||
ARM::State ToGenericContext() override {
|
||||
return cpu;
|
||||
}
|
||||
|
||||
void FromGenericContext(const ARM::State& state) override {
|
||||
// TODO: Don't use memcpy for copying?
|
||||
std::memcpy(&cpu, &state, sizeof(state));
|
||||
}
|
||||
|
||||
std::optional<uint32_t> TranslateVirtualAddress(uint32_t address) {
|
||||
return ExecutionContextWithDefaultMemory::TranslateVirtualAddress(address);
|
||||
}
|
||||
|
||||
void SetDebuggingEnabled(bool enabled = true) override {
|
||||
debugger_attached = enabled;
|
||||
}
|
||||
|
||||
bool IsDebuggingEnabled() const override {
|
||||
return debugger_attached;
|
||||
}
|
||||
|
||||
// Optional reference to coroutine
|
||||
// TODO: Fix for HostThreadBasedThreadControl
|
||||
boost::coroutines2::coroutine<uint32_t>::push_type* coro = nullptr;
|
||||
|
||||
// Tagged address for LDREX/STREX.
|
||||
// We perform an implicit CLREX on context switches, so this mainly ensures
|
||||
// that the STREX following a LDREX uses a matching address.
|
||||
std::optional<uint32_t> monitor_address;
|
||||
};
|
||||
|
||||
} // namespace Interpreter
|
485
source/arm/thumb.cpp
Normal file
485
source/arm/thumb.cpp
Normal file
|
@ -0,0 +1,485 @@
|
|||
#include "thumb.hpp"
|
||||
|
||||
namespace ARM {
|
||||
|
||||
DecodedThumbInstr DecodeThumb(ARM::ThumbInstr instr) {
|
||||
if (instr.opcode_upper5 == 0b00000) {
|
||||
// LSL (1) - Logical Shift Left
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = ((0b1110'0001'1011'0000ul) << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| (uint32_t { instr.immed_mid_5 } << 7)
|
||||
| instr.idx_rm;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper5 == 0b00010) {
|
||||
// ASR (1) - Arithmetic Shift Right
|
||||
ARM::ARMInstr arm_instr;
|
||||
// arm_instr.raw = (0b1110'0001'1011'0000ul << 16)
|
||||
// | (uint32_t { instr.idx_rd_low } << 12)
|
||||
// | (uint32_t { instr.immed_mid_5 } << 7)
|
||||
// | (0b100ul << 3)
|
||||
// | instr.idx_rm;
|
||||
arm_instr.raw = (0b1110'0001'1011'0000ul << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| (uint32_t { instr.immed_mid_5 } << 7)
|
||||
| (0b100ul << 4)
|
||||
| instr.idx_rm;
|
||||
// arm_instr.raw = (0b1110'0001'1011'0000'0000'1111'1010'0000ul); // shift_imm = 31, shift = 1
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper10 == 0b0100'0000'00) {
|
||||
// AND
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0000'0001ul << 20)
|
||||
| (uint32_t { instr.idx_rd_low } << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| instr.idx_rm;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper10 == 0b0100'0001'01) {
|
||||
// ADC
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0000'1011ul << 20)
|
||||
| (uint32_t { instr.idx_rd_low } << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| instr.idx_rm;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper7 == 0b0001110) {
|
||||
// ADD (1)
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0010'1001ul << 20)
|
||||
| (uint32_t { instr.idx_rm } << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| instr.immed_mid_5_lower3;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper5 == 0b00110) {
|
||||
// ADD (2)
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0010'1001ul << 20)
|
||||
| (uint32_t { instr.idx_rd_high } << 16)
|
||||
| (uint32_t { instr.idx_rd_high } << 12)
|
||||
| instr.immed_low_8;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper7 == 0b0001100) {
|
||||
// ADD (3)
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0000'1001ul << 20)
|
||||
| (uint32_t { instr.idx_rm } << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| instr.idx_rn;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper8 == 0b01000100) {
|
||||
// ADD (4)
|
||||
ARM::ARMInstr arm_instr;
|
||||
auto idx_rd = uint32_t { instr.idx_rd_low } | (uint32_t { instr.idx_rd_upperbit } << 3);
|
||||
auto idx_rm = uint32_t { instr.idx_rm } | (uint32_t { instr.idx_rm_upperbit } << 3);
|
||||
arm_instr.raw = (0b1110'0000'1000ul << 20)
|
||||
| (idx_rd << 16)
|
||||
| (idx_rd << 12)
|
||||
| idx_rm;
|
||||
auto ret = DecodedThumbInstr { arm_instr }.SetMayReadPC();
|
||||
if (idx_rd == 15) {
|
||||
ret.SetMayModifyPC();
|
||||
}
|
||||
return ret;
|
||||
// } else if (instr.opcode_upper5 == 0b10100) {
|
||||
// // ADD (5)
|
||||
// ARM::ARMInstr arm_instr;
|
||||
// arm_instr.raw = (0b1110'0010'1000'1111ul << 16)
|
||||
// | (uint32_t { instr.idx_rd_high } << 12)
|
||||
// | (0b1111ul << 8)
|
||||
// | instr.immed_low_8;
|
||||
// return DecodedThumbInstr { arm_instr }.SetMayReadPC();
|
||||
} else if (instr.opcode_upper5 == 0b10101) {
|
||||
// ADD (6)
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0010'1000'1101ul << 16)
|
||||
| (uint32_t { instr.idx_rd_high } << 12)
|
||||
| (0b1111ul << 8)
|
||||
| instr.immed_low_8;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper9 == 0b1011'0000'0) {
|
||||
// ADD (7)
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110001010001101110111110ul << 7)
|
||||
| instr.immed_low_7;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper10 == 0b0100'0011'10) {
|
||||
// BIC
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b111000011101ul << 20)
|
||||
| (uint32_t { instr.idx_rd_low } << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| instr.idx_rm;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper10 == 0b0100'0010'00) {
|
||||
// TST
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b111000010001ul << 20)
|
||||
| (uint32_t { instr.idx_rd_low } << 16)
|
||||
| instr.idx_rm;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper7 == 0b0001'111) {
|
||||
// SUB (1)
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b111000100101ul << 20)
|
||||
| (uint32_t { instr.idx_rm } << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| instr.immed_mid_5_lower3;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper5 == 0b00111) {
|
||||
// SUB (2)
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0010'0101ul << 20)
|
||||
| (uint32_t { instr.idx_rd_high } << 16)
|
||||
| (uint32_t { instr.idx_rd_high } << 12)
|
||||
| instr.immed_low_8;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper7 == 0b0001'101) {
|
||||
// SUB (3)
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0000'0101ul << 20)
|
||||
| (uint32_t { instr.idx_rm } << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| instr.idx_rn;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper9 == 0b1011'0000'1) {
|
||||
// SUB (4)
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0010'0100'1101'1101'1111'0ul << 7)
|
||||
| instr.immed_low_7;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper10 == 0b0100'0001'10) {
|
||||
// SBC
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0000'1101ul << 20)
|
||||
| (uint32_t { instr.idx_rd_low } << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| instr.idx_rm;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper10 == 0b0100'0011'00) {
|
||||
// ORR
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0001'1001ul << 20)
|
||||
| (uint32_t { instr.idx_rd_low } << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| instr.idx_rm;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper10 == 0b0100'0000'01) {
|
||||
// EOR
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0000'0011ul << 20)
|
||||
| (uint32_t { instr.idx_rd_low } << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| instr.idx_rm;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper5 == 0b00100) {
|
||||
// MOV (1)
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0011'1011'0000ul << 16)
|
||||
| (uint32_t { instr.idx_rd_high } << 12)
|
||||
| instr.immed_low_8;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper8 == 0b0100'0110) {
|
||||
// MOV (3)
|
||||
ARM::ARMInstr arm_instr;
|
||||
auto idx_rd = uint32_t { instr.idx_rd_low } | (uint32_t { instr.idx_rd_upperbit } << 3);
|
||||
auto idx_rm = uint32_t { instr.idx_rm } | (uint32_t { instr.idx_rm_upperbit } << 3);
|
||||
arm_instr.raw = (0b1110'0001'1010ul << 20)
|
||||
| (idx_rd << 12)
|
||||
| idx_rm;
|
||||
auto ret = DecodedThumbInstr { arm_instr }.SetMayReadPC();
|
||||
if (idx_rd == 15) {
|
||||
ret.SetMayModifyPC();
|
||||
}
|
||||
return ret;
|
||||
} else if (instr.opcode_upper10 == 0b0100'0011'11) {
|
||||
// MVN
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0001'1111'0000ul << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| instr.idx_rm;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper5 == 0b00000) {
|
||||
// LSL (1)
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0001'1011'0000ul << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| (uint32_t { instr.immed_mid_5 } << 7)
|
||||
| instr.idx_rm;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper10 == 0b0100'0000'10) {
|
||||
// LSL (2)
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0001'1011'0000ul << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| (uint32_t { instr.idx_rm } << 8)
|
||||
| 0b1'0000
|
||||
| instr.idx_rd_low;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper5 == 0b00001) {
|
||||
// LSR (1)
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0001'1011'0000ul << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| (uint32_t { instr.immed_mid_5 } << 7)
|
||||
| 0b10'0000
|
||||
| instr.idx_rm;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper10 == 0b0100'0000'11) {
|
||||
// LSR (2)
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0001'1011'0000ul << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| (uint32_t { instr.idx_rm } << 8)
|
||||
| 0b11'0000
|
||||
| instr.idx_rd_low;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper10 == 0b0100'0010'01) {
|
||||
// NEG
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0010'0111ul << 20)
|
||||
| (uint32_t { instr.idx_rm } << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12);
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper10 == 0b0100'0011'01) {
|
||||
// MUL
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0000'0001ul << 20)
|
||||
| (uint32_t { instr.idx_rd_low } << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 8)
|
||||
| (0b1001ul << 4)
|
||||
| instr.idx_rm;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper10 == 0b1011'1010'00) {
|
||||
// REV
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0110'1011'1111ul << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| (0b1111'0011ul << 4)
|
||||
| instr.idx_rm;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper10 == 0b0100'0001'11) {
|
||||
// ROR
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0001'1011'0000ul << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| (uint32_t { instr.idx_rm } << 8)
|
||||
| (0b0111ul << 4)
|
||||
| instr.idx_rd_low;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper10 == 0b1011'0010'01) {
|
||||
// SXTB
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0110'1010'1111ul << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| (0b0111ul << 4)
|
||||
| instr.idx_rm;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper10 == 0b1011'0010'00) {
|
||||
// SXTH
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0110'1011'1111ul << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| (0b0111ul << 4)
|
||||
| instr.idx_rm;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper10 == 0b1011'0010'10) {
|
||||
// UXTH
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0110'1111'1111ul << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| (0b0111ul << 4)
|
||||
| instr.idx_rm;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper10 == 0b1011'0010'11) {
|
||||
// UXTB
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0110'1110'1111ul << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| (0b0111ul << 4)
|
||||
| instr.idx_rm;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper5 == 0b11001) {
|
||||
// LDMIA
|
||||
ARM::ARMInstr arm_instr;
|
||||
uint32_t has_rd = (instr.register_list >> instr.idx_rd_high) & 1;
|
||||
arm_instr.raw = (0b1110'1000'1001ul << 20)
|
||||
| (uint32_t { !has_rd } << 21)
|
||||
| (uint32_t { instr.idx_rd_high } << 16)
|
||||
| instr.register_list;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper5 == 0b01101) {
|
||||
// LDR (1)
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0101'1001ul << 20)
|
||||
| (uint32_t { instr.idx_rm } << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| (uint32_t { instr.immed_mid_5 } << 2);
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper7 == 0b0101'100) {
|
||||
// LDR (2)
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0111'1001ul << 20)
|
||||
| (uint32_t { instr.idx_rm } << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| instr.idx_rn;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper5 == 0b10011) {
|
||||
// LDR (4)
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0101'1001'1101ul << 16)
|
||||
| (uint32_t { instr.idx_rd_high } << 12)
|
||||
| (uint32_t { instr.immed_low_8 } << 2);
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper5 == 0b01111) {
|
||||
// LDRB (1)
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0101'1101ul << 20)
|
||||
| (uint32_t { instr.idx_rm } << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| instr.immed_mid_5;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper7 == 0b0101110) {
|
||||
// LDRB (2)
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0111'1101ul << 20)
|
||||
| (uint32_t { instr.idx_rm } << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| instr.idx_rn;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper7 == 0b0101011) {
|
||||
// LDRSB
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0001'1001ul << 20)
|
||||
| (uint32_t { instr.idx_rm } << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| (0b1101ul << 4)
|
||||
| instr.idx_rn;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper7 == 0b0101111) {
|
||||
// LDRSH
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0001'1001ul << 20)
|
||||
| (uint32_t { instr.idx_rm } << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| (0b1111ul << 4)
|
||||
| instr.idx_rn;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper5 == 0b10001) {
|
||||
// LDRH (1)
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0001'1101ul<< 20)
|
||||
| (uint32_t { instr.idx_rm } << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| (uint32_t { instr.immed_mid_5_upper2 } << 8)
|
||||
| (0b1011ul << 4)
|
||||
| (uint32_t { instr.immed_mid_5_lower3 } << 1);
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper7 == 0b0101'101) {
|
||||
// LDRH (2)
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0001'1001ul<< 20)
|
||||
| (uint32_t { instr.idx_rm } << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| (0b1011ul << 4)
|
||||
| instr.idx_rn;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper5 == 0b00101) {
|
||||
// CMP (1) - Compare
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0011'0101ul << 20)
|
||||
| (uint32_t { instr.idx_rd_high } << 16)
|
||||
| (instr.immed_low_8);
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper10 == 0b0100'0010'10) {
|
||||
// CMP (2) - Compare
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b111000010101ul << 20)
|
||||
| (uint32_t { instr.idx_rd_low } << 16)
|
||||
| instr.idx_rm;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper8 == 0b0100'0101) {
|
||||
// CMP (3) - Compare
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b111000010101ul << 20)
|
||||
| (uint32_t { instr.idx_rd_low } << 16)
|
||||
| instr.idx_rm;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper5 == 0b11000) {
|
||||
// STMIA
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'1000'1010ul << 20)
|
||||
| (uint32_t { instr.idx_rd_high } << 16)
|
||||
| instr.register_list;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper5 == 0b01100) {
|
||||
// STR (1)
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0101'1000ul << 20)
|
||||
| (uint32_t { instr.idx_rm } << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| (uint32_t { instr.immed_mid_5 } << 2);
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper7 == 0b0101000) {
|
||||
// STR (2)
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0111'1000ul << 20)
|
||||
| (uint32_t { instr.idx_rm } << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| instr.idx_rn;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper5 == 0b10010) {
|
||||
// STR (3)
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0101'1000'1101ul << 16)
|
||||
| (uint32_t { instr.idx_rd_high } << 12)
|
||||
| (uint32_t { instr.immed_low_8 } << 2);
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper5 == 0b01110) {
|
||||
// STRB (1)
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0101'1100ul << 20)
|
||||
| (uint32_t { instr.idx_rm } << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| instr.immed_mid_5;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper7 == 0b0101'010) {
|
||||
// STRB (2)
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0111'1100ul << 20)
|
||||
| (uint32_t { instr.idx_rm } << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| instr.idx_rn;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper5 == 0b10000) {
|
||||
// STRH (1) - Store Register Halfword
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0001'1100ul << 20)
|
||||
| (uint32_t { instr.idx_rm } << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| (uint32_t { instr.immed_mid_5_upper2 } << 8)
|
||||
| (0b1011 << 4)
|
||||
| (uint32_t { instr.immed_mid_5_lower3 } << 1);
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper7 == 0b0101001) {
|
||||
// STRH (2) - Store Register Halfword
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'0001'1000ul << 20)
|
||||
| (uint32_t { instr.idx_rm } << 16)
|
||||
| (uint32_t { instr.idx_rd_low } << 12)
|
||||
| (0b1011 << 4)
|
||||
| instr.idx_rn;
|
||||
return { arm_instr };
|
||||
} else if (instr.opcode_upper7 == 0b1011'010) {
|
||||
// PUSH - Push Multiple Registers
|
||||
ARM::ARMInstr arm_instr;
|
||||
arm_instr.raw = (0b1110'1001'0010'1101ul << 16)
|
||||
| ((uint32_t { instr.raw } & 0x100) << 6) // bit8 denotes whether to push LR
|
||||
| instr.register_list;
|
||||
return { arm_instr };
|
||||
} else {
|
||||
// Instruction has no ARM equivalent, let the caller handle it manually
|
||||
return { };
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ARM
|
33
source/arm/thumb.hpp
Normal file
33
source/arm/thumb.hpp
Normal file
|
@ -0,0 +1,33 @@
|
|||
#pragma once
|
||||
|
||||
#include "../arm.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace ARM {
|
||||
|
||||
struct DecodedThumbInstr {
|
||||
// Equivalent ARM instruction, if any
|
||||
std::optional<ARM::ARMInstr> arm_equivalent;
|
||||
bool may_read_pc = false;
|
||||
bool may_modify_pc = false;
|
||||
|
||||
DecodedThumbInstr& SetMayReadPC() {
|
||||
may_read_pc = true;
|
||||
return *this;
|
||||
}
|
||||
|
||||
DecodedThumbInstr& SetMayModifyPC() {
|
||||
may_modify_pc = true;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Translates the given THUMB instruction to an equivalent ARM encoding.
|
||||
* If this is not possible, the caller is responsible for interpreting
|
||||
* the instruction manually.
|
||||
*/
|
||||
DecodedThumbInstr DecodeThumb(ARM::ThumbInstr);
|
||||
|
||||
} // namespace ARM
|
219
source/bit_field.h
Normal file
219
source/bit_field.h
Normal file
|
@ -0,0 +1,219 @@
|
|||
// Copyright 2014 Tony Wasserka
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above copyright
|
||||
// notice, this list of conditions and the following disclaimer in the
|
||||
// documentation and/or other materials provided with the distribution.
|
||||
// * Neither the name of the owner nor the names of its contributors may
|
||||
// be used to endorse or promote products derived from this software
|
||||
// without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
#include <type_traits>
|
||||
|
||||
namespace BitField {
|
||||
|
||||
namespace v1 {
|
||||
|
||||
/**
|
||||
* Abstract bitfield class
|
||||
*
|
||||
* Allows endianness-independent access to individual bitfields within some raw
|
||||
* integer value. The assembly generated by this class is identical to the
|
||||
* usage of raw bitfields, so it's a perfectly fine replacement.
|
||||
*
|
||||
* For BitField<X,Y,Z>, X is the distance of the bitfield to the LSB of the
|
||||
* raw value, Y is the length in bits of the bitfield. Z is an integer type
|
||||
* which determines the sign of the bitfield. Z must have the same size as the
|
||||
* raw integer.
|
||||
*
|
||||
* \par General usage
|
||||
*
|
||||
* Create a new union with the raw integer value as a member.
|
||||
* Then for each bitfield you want to expose, add a BitField member
|
||||
* in the union. The template parameters are the bit offset and the number
|
||||
* of desired bits.
|
||||
*
|
||||
* Changes in the bitfield members will then get reflected in the raw integer
|
||||
* value and vice-versa.
|
||||
*
|
||||
* \par Sample usage
|
||||
*
|
||||
* \code
|
||||
* union SomeRegister
|
||||
* {
|
||||
* u32 hex;
|
||||
*
|
||||
* BitField<0,7,u32> first_seven_bits; // unsigned
|
||||
* BitField<7,8,u32> next_eight_bits; // unsigned
|
||||
* BitField<3,15,s32> some_signed_fields; // signed
|
||||
* };
|
||||
* \endcode
|
||||
*
|
||||
* This is equivalent to the little-endian specific code:
|
||||
* \code
|
||||
* union SomeRegister
|
||||
* {
|
||||
* u32 hex;
|
||||
*
|
||||
* struct
|
||||
* {
|
||||
* u32 first_seven_bits : 7;
|
||||
* u32 next_eight_bits : 8;
|
||||
* };
|
||||
* struct
|
||||
* {
|
||||
* u32 : 3; // padding
|
||||
* s32 some_signed_fields : 15;
|
||||
* };
|
||||
* };
|
||||
* \endcode
|
||||
*
|
||||
*
|
||||
* \par Caveats
|
||||
*
|
||||
* - BitField provides automatic casting from and to the storage type where
|
||||
* appropriate. However, when using non-typesafe functions like printf, an
|
||||
* explicit cast must be performed on the BitField object to make sure it gets
|
||||
* passed correctly, e.g.:
|
||||
* \code
|
||||
* printf("Value: %d", (s32)some_register.some_signed_fields);
|
||||
* \endcode
|
||||
*
|
||||
* - Not really a caveat, but potentially irritating: This class is used in some
|
||||
* packed structures that do not guarantee proper alignment. Therefore we have
|
||||
* to use \code #pragma pack \endcode here not to pack the members of the class, but instead
|
||||
* to break GCC's assumption that the members of the class are aligned on
|
||||
* sizeof(StorageType). \todo Confirm that this is a proper fix and not just masking
|
||||
* symptoms.
|
||||
*/
|
||||
#pragma pack(1)
|
||||
template<std::size_t position, std::size_t bits, typename T>
|
||||
struct BitField
|
||||
{
|
||||
private:
|
||||
// This constructor might be considered ambiguous:
|
||||
// Would it initialize the storage or just the bitfield?
|
||||
// Hence, delete it. Use the assignment operator to set bitfield values!
|
||||
BitField(T val) = delete;
|
||||
|
||||
public:
|
||||
// Force default constructor to be created
|
||||
// so that we can use this within unions
|
||||
BitField() = default;
|
||||
|
||||
#ifndef _WIN32
|
||||
// We explicitly delete the copy assigment operator here, because the
|
||||
// default copy assignment would copy the full storage value, rather than
|
||||
// just the bits relevant to this particular bit field.
|
||||
// Ideally, we would just implement the copy assignment to copy only the
|
||||
// relevant bits, but this requires compiler support for unrestricted
|
||||
// unions.
|
||||
// MSVC 2013 has no support for this, hence we disable this code on
|
||||
// Windows (so that the default copy assignment operator will be used).
|
||||
// For any C++11 conformant compiler we delete the operator to make sure
|
||||
// we never use this inappropriate operator to begin with.
|
||||
// TODO: Implement this operator properly once all target compilers
|
||||
// support unrestricted unions.
|
||||
BitField& operator=(const BitField&) = delete;
|
||||
#endif
|
||||
|
||||
inline BitField& operator=(T val)
|
||||
{
|
||||
Assign(val);
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline operator T() const
|
||||
{
|
||||
return Value();
|
||||
}
|
||||
|
||||
inline void Assign(const T& value) {
|
||||
storage = (storage & ~GetMask()) | (((StorageType)value << position) & GetMask());
|
||||
}
|
||||
|
||||
inline T Value() const
|
||||
{
|
||||
if (std::numeric_limits<T>::is_signed)
|
||||
{
|
||||
std::size_t shift = 8 * sizeof(T) - bits;
|
||||
return (T)(static_cast<StorageType>(storage << (shift - position)) >> shift);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (T)((storage & GetMask()) >> position);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: we may want to change this to explicit operator bool() if it's bug-free in VS2015
|
||||
inline bool ToBool() const
|
||||
{
|
||||
return Value() != 0;
|
||||
}
|
||||
|
||||
private:
|
||||
// StorageType is T for non-enum types and the underlying type of T if
|
||||
// T is an enumeration. Note that T is wrapped within an enable_if in the
|
||||
// former case to workaround compile errors which arise when using
|
||||
// std::underlying_type<T>::type directly.
|
||||
typedef typename std::conditional < std::is_enum<T>::value,
|
||||
std::underlying_type<T>,
|
||||
std::enable_if < true, T >> ::type::type StorageType;
|
||||
|
||||
// Unsigned version of StorageType
|
||||
typedef typename std::make_unsigned<StorageType>::type StorageTypeU;
|
||||
|
||||
inline StorageType GetMask() const
|
||||
{
|
||||
return (((StorageTypeU)~0) >> (8 * sizeof(T)-bits)) << position;
|
||||
}
|
||||
|
||||
StorageType storage;
|
||||
|
||||
static_assert(bits + position <= 8 * sizeof(T), "Bitfield out of range");
|
||||
|
||||
// And, you know, just in case people specify something stupid like bits=position=0x80000000
|
||||
static_assert(position < 8 * sizeof(T), "Invalid position");
|
||||
static_assert(bits <= 8 * sizeof(T), "Invalid number of bits");
|
||||
static_assert(bits > 0, "Invalid number of bits");
|
||||
static_assert(std::is_standard_layout<T>::value, "Invalid base type");
|
||||
};
|
||||
#pragma pack()
|
||||
|
||||
template<std::size_t position, std::size_t bits, typename T, typename T2=T>
|
||||
const BitField<position, bits, T>& ViewBitField(const T2& raw) {
|
||||
static_assert(sizeof(T) == sizeof(T2), "Given types must be of same size");
|
||||
return reinterpret_cast<const BitField<position, bits, T>&>(raw);
|
||||
}
|
||||
|
||||
template<std::size_t position, std::size_t bits, typename T, typename T2=T>
|
||||
BitField<position, bits, T>& ViewBitField(T2& raw) {
|
||||
static_assert(sizeof(T) == sizeof(T2), "Given types must be of same size");
|
||||
return reinterpret_cast<BitField<position, bits, T>&>(raw);
|
||||
}
|
||||
|
||||
} // namespace v1
|
||||
|
||||
} // namespace BitField
|
108
source/debug/jit.cpp
Normal file
108
source/debug/jit.cpp
Normal file
|
@ -0,0 +1,108 @@
|
|||
#include "jit.hpp"
|
||||
|
||||
#ifndef __ANDROID__
|
||||
#include <pistache/router.h>
|
||||
#endif
|
||||
|
||||
#include <range/v3/view/enumerate.hpp>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
using namespace Pistache;
|
||||
|
||||
// TODO: Move to common header...
|
||||
struct CompiledBlockInfo {
|
||||
uint32_t instrs_arm;
|
||||
uint32_t instrs_llvm;
|
||||
};
|
||||
|
||||
// TODO: Move to common header...
|
||||
struct JitDebugInfo {
|
||||
std::map<uint32_t, CompiledBlockInfo> compiled_blocks[2];
|
||||
};
|
||||
|
||||
namespace Debugger {
|
||||
|
||||
void JitService::RegisterContext(HLE::OS::ProcessId pid, HLE::OS::ThreadId tid, JitDebugInfo& jit_context) {
|
||||
// TODO: Assert it's a new one
|
||||
contexts.emplace(JitId { pid, tid }, &jit_context);
|
||||
}
|
||||
|
||||
void JitService::UnregisterContext(HLE::OS::ProcessId pid, HLE::OS::ThreadId tid) {
|
||||
contexts.erase(JitId { pid, tid });
|
||||
}
|
||||
|
||||
#ifndef __ANDROID__
|
||||
void JitService::doOptions(const Rest::Request&, Http::ResponseWriter response) {
|
||||
response.headers().add<Http::Header::AccessControlAllowOrigin>("*");
|
||||
response.headers().add<Http::Header::AccessControlAllowHeaders>("user-agent");
|
||||
response.send(Http::Code::No_Content);
|
||||
}
|
||||
|
||||
void JitService::doJitBlocks(const Rest::Request& request, Http::ResponseWriter response) {
|
||||
auto id = JitId::FromRaw(request.param(":id").as<uint64_t>());
|
||||
auto it = contexts.find(id);
|
||||
if (it == contexts.end()) {
|
||||
auto [pid, tid] = id.Decode();
|
||||
throw std::runtime_error(fmt::format("Tried to access invalid JIT with pid {} and tid {}", pid, tid));
|
||||
}
|
||||
|
||||
response.headers().add<Http::Header::AccessControlAllowOrigin>("*");
|
||||
|
||||
// Iterate over compiled blocks, merging the ARM and Thumb blocks while sorting by their starting address
|
||||
std::string body = "[";
|
||||
std::size_t block_id = 0;
|
||||
auto block_it_arm = it->second->compiled_blocks[0].begin();
|
||||
auto block_it_thumb = it->second->compiled_blocks[1].begin();
|
||||
const auto block_it_arm_end = it->second->compiled_blocks[0].end();
|
||||
const auto block_it_thumb_end = it->second->compiled_blocks[1].end();
|
||||
while ( block_it_arm != block_it_arm_end ||
|
||||
block_it_thumb != block_it_thumb_end) {
|
||||
bool is_thumb;
|
||||
|
||||
if (block_it_thumb == block_it_thumb_end) {
|
||||
is_thumb = false;
|
||||
} else if (block_it_arm == block_it_arm_end) {
|
||||
is_thumb = true;
|
||||
} else if (block_it_arm->first <= block_it_thumb->first) {
|
||||
is_thumb = false;
|
||||
} else {
|
||||
is_thumb = true;
|
||||
}
|
||||
|
||||
auto block_it = (is_thumb ? block_it_thumb++ : block_it_arm++);
|
||||
|
||||
uint32_t addr = block_it->first;
|
||||
auto& block = block_it->second;
|
||||
body += fmt::format("{{ \"id\": {}, \"addr\": {}, \"thumb\": {}, \"instrs_native\": {}, \"instrs_ir\": {}, \"instrs_compiled\": 0 }},",
|
||||
++block_id, addr, is_thumb ? "true" : "false", block.instrs_arm, block.instrs_llvm);
|
||||
}
|
||||
|
||||
// Drop trailing comma
|
||||
if (body.size() > 1) {
|
||||
body.pop_back();
|
||||
}
|
||||
|
||||
response.send(Http::Code::Ok, body + "]");
|
||||
}
|
||||
#endif
|
||||
|
||||
void JitService::RegisterRoutes(Rest::Router& router) {
|
||||
#ifndef __ANDROID__
|
||||
using namespace Rest;
|
||||
|
||||
Routes::Get(router, "/jit/:id/blocks", Routes::bind(&JitService::doJitBlocks, this));
|
||||
|
||||
// TODO: Move those to DebugServer itself
|
||||
Routes::Options(router, "/jit/*/*", Routes::bind(&JitService::doOptions, this));
|
||||
Routes::Options(router, "/jit/*", Routes::bind(&JitService::doOptions, this));
|
||||
Routes::Options(router, "/jit", Routes::bind(&JitService::doOptions, this));
|
||||
#endif
|
||||
}
|
||||
|
||||
template<>
|
||||
std::unique_ptr<Service> CreateService<JitService>() {
|
||||
return std::make_unique<JitService>();
|
||||
}
|
||||
|
||||
} // namespace Debugger
|
60
source/debug/jit.hpp
Normal file
60
source/debug/jit.hpp
Normal file
|
@ -0,0 +1,60 @@
|
|||
#pragma once
|
||||
|
||||
#include <debug_server.hpp>
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
namespace HLE::OS {
|
||||
using ProcessId = uint32_t;
|
||||
using ThreadId = uint32_t;
|
||||
}
|
||||
|
||||
struct JitDebugInfo;
|
||||
|
||||
namespace Debugger {
|
||||
|
||||
class JitId {
|
||||
uint64_t value;
|
||||
|
||||
JitId(uint64_t value) : value(value) { }
|
||||
|
||||
public:
|
||||
JitId(HLE::OS::ProcessId pid, HLE::OS::ThreadId tid) : value { (uint64_t { pid } << 32) | tid } {
|
||||
|
||||
}
|
||||
|
||||
static JitId FromRaw(uint64_t value) {
|
||||
return JitId { value };
|
||||
}
|
||||
|
||||
std::pair<HLE::OS::ProcessId, HLE::OS::ThreadId> Decode() const {
|
||||
HLE::OS::ProcessId pid = value >> 32;
|
||||
HLE::OS::ThreadId tid = value & 0xffffffff;
|
||||
return std::make_pair(pid, tid);
|
||||
}
|
||||
|
||||
struct Hasher {
|
||||
auto operator()(JitId self) const {
|
||||
return std::hash<uint64_t>{}(self.value);
|
||||
}
|
||||
};
|
||||
|
||||
bool operator==(JitId oth) const {
|
||||
return value == oth.value;
|
||||
}
|
||||
};
|
||||
|
||||
struct JitService : Service {
|
||||
std::unordered_map<JitId, JitDebugInfo*, JitId::Hasher> contexts;
|
||||
|
||||
void RegisterContext(HLE::OS::ProcessId pid, HLE::OS::ThreadId tid, JitDebugInfo& jit_context);
|
||||
void UnregisterContext(HLE::OS::ProcessId pid, HLE::OS::ThreadId tid);
|
||||
|
||||
void doOptions(const Pistache::Rest::Request&, Pistache::Http::ResponseWriter response);
|
||||
|
||||
void doJitBlocks(const Pistache::Rest::Request& request, Pistache::Http::ResponseWriter response);
|
||||
|
||||
void RegisterRoutes(Pistache::Rest::Router& router) override;
|
||||
};
|
||||
|
||||
} // namespace Debugger
|
169
source/debug/os.cpp
Normal file
169
source/debug/os.cpp
Normal file
|
@ -0,0 +1,169 @@
|
|||
#include "os.hpp"
|
||||
#include <os.hpp>
|
||||
|
||||
#ifndef __ANDROID__
|
||||
#include <pistache/router.h>
|
||||
#endif
|
||||
|
||||
#include <range/v3/algorithm/copy.hpp>
|
||||
#include <range/v3/iterator/insert_iterators.hpp>
|
||||
|
||||
namespace Debugger {
|
||||
|
||||
void OSService::RegisterProcess(HLE::OS::ProcessId pid, HLE::OS::Process& process) {
|
||||
std::lock_guard guard(access_mutex);
|
||||
|
||||
processes[pid].process = &process;
|
||||
}
|
||||
|
||||
void OSService::UnregisterProcess(HLE::OS::ProcessId pid) {
|
||||
std::lock_guard guard(access_mutex);
|
||||
|
||||
processes.erase(pid);
|
||||
}
|
||||
|
||||
void OSService::RegisterThread(HLE::OS::ProcessId pid, HLE::OS::ThreadId tid, HLE::OS::Thread& thread) {
|
||||
std::lock_guard guard(access_mutex);
|
||||
|
||||
processes[pid].threads[tid] = &thread;
|
||||
}
|
||||
|
||||
void OSService::UnregisterThread(HLE::OS::ProcessId pid, HLE::OS::ThreadId tid) {
|
||||
std::lock_guard guard(access_mutex);
|
||||
|
||||
processes[pid].threads.erase(tid);
|
||||
}
|
||||
|
||||
void OSService::Shutdown() {
|
||||
std::lock_guard guard(access_mutex);
|
||||
|
||||
processes.clear();
|
||||
}
|
||||
|
||||
#ifndef __ANDROID__
|
||||
using namespace Pistache;
|
||||
|
||||
static void doOptions(const Pistache::Rest::Request&, Pistache::Http::ResponseWriter response) {
|
||||
response.headers().add<Http::Header::AccessControlAllowOrigin>("*");
|
||||
response.headers().add<Http::Header::AccessControlAllowHeaders>("user-agent");
|
||||
response.send(Http::Code::No_Content);
|
||||
}
|
||||
|
||||
static void doProcessList(OSService& service, const Pistache::Rest::Request&, Pistache::Http::ResponseWriter response) {
|
||||
std::lock_guard guard(service.access_mutex);
|
||||
|
||||
response.headers().add<Http::Header::AccessControlAllowOrigin>("*");
|
||||
response.headers().add<Http::Header::ContentType>(Http::Mime::MediaType { Http::Mime::Type::Application, Http::Mime::Subtype::Json });
|
||||
|
||||
std::string body = "[";
|
||||
for (auto& pid_and_process : service.processes) {
|
||||
auto& process_info = pid_and_process.second;
|
||||
|
||||
// TODO: EmuProcess vs FakeProcess
|
||||
body += fmt::format(R"({{ "id": {}, "name": "{}", "threadcount": {} }},)", pid_and_process.first, process_info.process->GetName(), process_info.threads.size());
|
||||
}
|
||||
if (!service.processes.empty()) {
|
||||
// Drop trailing comma
|
||||
body.pop_back();
|
||||
}
|
||||
|
||||
response.send(Http::Code::Ok, body + "]");
|
||||
}
|
||||
|
||||
static void doHandleTable(OSService& service, const Pistache::Rest::Request& request, Pistache::Http::ResponseWriter response) {
|
||||
std::lock_guard guard(service.access_mutex);
|
||||
|
||||
auto pid = request.param(":pid").as<HLE::OS::ProcessId>();
|
||||
if (!service.processes.count(pid)) {
|
||||
throw std::runtime_error(fmt::format("Tried to access invalid process with pid {}", pid));
|
||||
}
|
||||
|
||||
response.headers().add<Http::Header::AccessControlAllowOrigin>("*");
|
||||
response.headers().add<Http::Header::ContentType>(Http::Mime::MediaType { Http::Mime::Type::Application, Http::Mime::Subtype::Json });
|
||||
|
||||
std::string body = "[";
|
||||
|
||||
namespace HOS = HLE::OS;
|
||||
|
||||
auto& process = *service.processes[pid].process;
|
||||
std::map<HOS::DebugHandle, std::shared_ptr<HOS::Object>, std::less<HOS::Handle>> sorted_handle_table;
|
||||
ranges::copy(process.handle_table.table, ranges::inserter(sorted_handle_table, ranges::begin(sorted_handle_table)));
|
||||
for (auto& handle : sorted_handle_table) {
|
||||
body += fmt::format(R"({{ "id": {}, "name": "{}" }},)", handle.first.value, handle.second->GetName());
|
||||
}
|
||||
if (!sorted_handle_table.empty()) {
|
||||
// Drop trailing comma
|
||||
body.pop_back();
|
||||
}
|
||||
response.send(Http::Code::Ok, body + "]");
|
||||
}
|
||||
|
||||
static void doProcessThreadList(OSService& service, const Pistache::Rest::Request& request, Pistache::Http::ResponseWriter response) {
|
||||
std::lock_guard guard(service.access_mutex);
|
||||
|
||||
auto pid = request.param(":pid").as<HLE::OS::ProcessId>();
|
||||
if (!service.processes.count(pid)) {
|
||||
throw std::runtime_error(fmt::format("Tried to access invalid process with pid {}", pid));
|
||||
}
|
||||
|
||||
response.headers().add<Http::Header::AccessControlAllowOrigin>("*");
|
||||
response.headers().add<Http::Header::ContentType>(Http::Mime::MediaType { Http::Mime::Type::Application, Http::Mime::Subtype::Json });
|
||||
|
||||
std::string body = "[";
|
||||
for (auto& tid_and_thread : service.processes[pid].threads) {
|
||||
auto& thread = *tid_and_thread.second;
|
||||
|
||||
const char* status = [&]() {
|
||||
switch (thread.status) {
|
||||
case HLE::OS::Thread::Status::Ready: return "Ready";
|
||||
case HLE::OS::Thread::Status::Sleeping: return "Sleeping";
|
||||
case HLE::OS::Thread::Status::WaitingForTimeout: return "WaitingForTimeout";
|
||||
case HLE::OS::Thread::Status::WaitingForArbitration: return "WaitingForArbitration";
|
||||
case HLE::OS::Thread::Status::Stopped: return "Stopped";
|
||||
}
|
||||
throw std::runtime_error("Unknown thread status");
|
||||
}();
|
||||
|
||||
// TODO: Send jitcontextid only for EmuThreads!
|
||||
auto emu_thread = dynamic_cast<HLE::OS::EmuThread*>(&thread);
|
||||
auto jitcontextid = 4294967297; // TODO: Retrieve from emu_thread
|
||||
body += fmt::format(R"({{ "id": {}, "pid": {}, "name": "{}", "status": "{}", "jitcontextid": {} }},)",
|
||||
tid_and_thread.first, thread.GetParentProcess().GetId(), thread.GetName(),
|
||||
status, jitcontextid);
|
||||
}
|
||||
if (!service.processes.empty()) {
|
||||
// Drop trailing comma
|
||||
body.pop_back();
|
||||
}
|
||||
response.send(Http::Code::Ok, body + "]");
|
||||
}
|
||||
#endif
|
||||
|
||||
void OSService::RegisterRoutes(Pistache::Rest::Router& router) {
|
||||
#ifndef __ANDROID__
|
||||
using namespace Rest;
|
||||
|
||||
Routes::Get(router, "/os/processes",
|
||||
[this](const auto& request, auto response) { doProcessList(*this, request, std::move(response)); return Route::Result::Ok; });
|
||||
Routes::Get(router, "/os/process/:pid/handletable",
|
||||
[this](const auto& request, auto response) { doHandleTable(*this, request, std::move(response)); return Route::Result::Ok; });
|
||||
Routes::Get(router, "/os/process/:pid/threads",
|
||||
[this](const auto& request, auto response) { doProcessThreadList(*this, request, std::move(response)); return Route::Result::Ok; });
|
||||
// Routes::Get(router, "/os/process/:pid/thread/:tid/registers",
|
||||
// [this](const auto& request, auto response) { doProcessThreadList(*this, request, std::move(response)); return Route::Result::Ok; });
|
||||
|
||||
// TODO: Interface to read process memory
|
||||
|
||||
// TODO: Move those to DebugServer itself
|
||||
// Routes::Options(router, "/os", Routes::bind(doOptions));
|
||||
Routes::Options(router, "/os/*", Routes::bind(doOptions));
|
||||
Routes::Options(router, "/os/*/*/*", Routes::bind(doOptions));
|
||||
#endif
|
||||
}
|
||||
|
||||
template<>
|
||||
std::unique_ptr<Service> CreateService<OSService>() {
|
||||
return std::make_unique<OSService>();
|
||||
}
|
||||
|
||||
} // namespace Debugger
|
38
source/debug/os.hpp
Normal file
38
source/debug/os.hpp
Normal file
|
@ -0,0 +1,38 @@
|
|||
#pragma once
|
||||
|
||||
#include <debug_server.hpp>
|
||||
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace HLE::OS {
|
||||
class OS;
|
||||
class Process;
|
||||
class Thread;
|
||||
using ProcessId = uint32_t;
|
||||
using ThreadId = uint32_t;
|
||||
}
|
||||
|
||||
namespace Debugger {
|
||||
|
||||
struct OSService : Service {
|
||||
struct ProcessContext {
|
||||
HLE::OS::Process* process;
|
||||
std::unordered_map<HLE::OS::ThreadId, HLE::OS::Thread*> threads;
|
||||
};
|
||||
|
||||
std::unordered_map<HLE::OS::ProcessId, ProcessContext> processes;
|
||||
std::mutex access_mutex;
|
||||
|
||||
void RegisterProcess(HLE::OS::ProcessId, HLE::OS::Process&);
|
||||
void UnregisterProcess(HLE::OS::ProcessId);
|
||||
|
||||
void RegisterThread(HLE::OS::ProcessId, HLE::OS::ThreadId, HLE::OS::Thread&);
|
||||
void UnregisterThread(HLE::OS::ProcessId, HLE::OS::ThreadId);
|
||||
|
||||
void Shutdown();
|
||||
|
||||
void RegisterRoutes(Pistache::Rest::Router&) override;
|
||||
};
|
||||
|
||||
} // namespace Debugger
|
74
source/debug_server.cpp
Normal file
74
source/debug_server.cpp
Normal file
|
@ -0,0 +1,74 @@
|
|||
#include "debug_server.hpp"
|
||||
|
||||
#include <pistache/endpoint.h>
|
||||
#include <pistache/router.h>
|
||||
|
||||
#include <range/v3/algorithm/find_if.hpp>
|
||||
#include <range/v3/view/transform.hpp>
|
||||
#include <range/v3/view/indirect.hpp>
|
||||
|
||||
#include <string>
|
||||
|
||||
using namespace Pistache;
|
||||
|
||||
namespace Debugger {
|
||||
|
||||
// Forward declare external services
|
||||
template<> std::unique_ptr<Service> CreateService<struct JitService>();
|
||||
template<> std::unique_ptr<Service> CreateService<struct OSService>();
|
||||
template<> std::unique_ptr<Service> CreateService<struct GPUService>();
|
||||
|
||||
class DebugServerImpl {
|
||||
std::vector<std::unique_ptr<Service>> services;
|
||||
|
||||
public:
|
||||
DebugServerImpl() {
|
||||
services.push_back(CreateService<GPUService>());
|
||||
services.push_back(CreateService<JitService>());
|
||||
services.push_back(CreateService<OSService>());
|
||||
}
|
||||
|
||||
void Run(unsigned port) {
|
||||
Address addr(Ipv4::any(), static_cast<uint16_t>(port));
|
||||
Http::Endpoint endpoint(addr);
|
||||
endpoint.init(Http::Endpoint::options().flags(Tcp::Options::NoDelay | Tcp::Options::ReuseAddr));
|
||||
Rest::Router router;
|
||||
|
||||
// Register services
|
||||
for (auto& service : services) {
|
||||
service->RegisterRoutes(router);
|
||||
}
|
||||
|
||||
// Run server
|
||||
endpoint.setHandler(router.handler());
|
||||
endpoint.serve();
|
||||
}
|
||||
|
||||
Service& GetService(const std::type_info& service_type) const {
|
||||
auto it = ranges::find_if(services,
|
||||
[&](const auto& service) -> bool {
|
||||
auto& the_service = *service;
|
||||
return (service_type == typeid(the_service));
|
||||
});
|
||||
if (it == services.end()) {
|
||||
throw std::runtime_error("Could not find service" + std::string { service_type.name() });
|
||||
}
|
||||
return **it;
|
||||
}
|
||||
};
|
||||
|
||||
DebugServer::DebugServer() {
|
||||
impl = std::make_unique<DebugServerImpl>();
|
||||
}
|
||||
|
||||
DebugServer::~DebugServer() = default;
|
||||
|
||||
void DebugServer::Run(unsigned port) {
|
||||
impl->Run(port);
|
||||
}
|
||||
|
||||
Service& DebugServer::GetService(const std::type_info& service_type) const {
|
||||
return impl->GetService(service_type);
|
||||
}
|
||||
|
||||
} // namespace Debugger
|
62
source/debug_server.hpp
Normal file
62
source/debug_server.hpp
Normal file
|
@ -0,0 +1,62 @@
|
|||
#pragma once
|
||||
|
||||
#if ENABLE_PISTACHE
|
||||
#define DEBUG_SERVER_AVAILABLE
|
||||
#endif
|
||||
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
#ifdef DEBUG_SERVER_AVAILABLE
|
||||
namespace Pistache {
|
||||
namespace Rest {
|
||||
class Request;
|
||||
class Router;
|
||||
}
|
||||
namespace Http {
|
||||
class ResponseWriter;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace Debugger {
|
||||
|
||||
#ifdef DEBUG_SERVER_AVAILABLE
|
||||
struct Service {
|
||||
virtual ~Service() = default;
|
||||
|
||||
virtual void RegisterRoutes(Pistache::Rest::Router& router) = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Factory function to create a service of the given type.
|
||||
* Must be specialized by service implementors
|
||||
*/
|
||||
template<typename ConcreteService>
|
||||
std::unique_ptr<Service> CreateService();
|
||||
|
||||
class DebugServerImpl;
|
||||
#endif
|
||||
|
||||
class DebugServer {
|
||||
#ifdef DEBUG_SERVER_AVAILABLE
|
||||
std::unique_ptr<DebugServerImpl> impl;
|
||||
|
||||
Service& GetService(const std::type_info& service_type) const;
|
||||
#endif
|
||||
|
||||
public:
|
||||
#ifdef DEBUG_SERVER_AVAILABLE
|
||||
DebugServer();
|
||||
~DebugServer();
|
||||
|
||||
void Run(unsigned port);
|
||||
|
||||
template<typename ConcreteService>
|
||||
ConcreteService& GetService() const {
|
||||
return static_cast<ConcreteService&>(GetService(typeid(ConcreteService)));
|
||||
}
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace Debugger
|
368
source/display.cpp
Normal file
368
source/display.cpp
Normal file
|
@ -0,0 +1,368 @@
|
|||
#define VULKAN_HPP_DISPATCH_LOADER_DYNAMIC 1
|
||||
|
||||
#include "display.hpp"
|
||||
|
||||
#include "framework/meta_tools.hpp"
|
||||
|
||||
#include <vulkan_utils/debug_markers.hpp>
|
||||
#include <vulkan_utils/glsl_helper.hpp>
|
||||
#include <vulkan_utils/memory_types.hpp>
|
||||
|
||||
#include <spdlog/logger.h>
|
||||
|
||||
#include <thread>
|
||||
|
||||
namespace EmuDisplay {
|
||||
|
||||
static vk::UniquePipeline CreateGraphicsPipelineForFullscreenEffect(
|
||||
uint32_t fb_width, uint32_t fb_height, vk::Device device, vk::RenderPass renderpass,
|
||||
vk::PipelineLayout layout, vk::ShaderModule vertex_shader, vk::ShaderModule pixel_shader) {
|
||||
std::array<vk::PipelineShaderStageCreateInfo, 2> shader_infos = {{
|
||||
{ vk::PipelineShaderStageCreateFlags { }, vk::ShaderStageFlagBits::eVertex, vertex_shader, "main" },
|
||||
{ vk::PipelineShaderStageCreateFlags { }, vk::ShaderStageFlagBits::eFragment, pixel_shader, "main" }
|
||||
}};
|
||||
|
||||
vk::PipelineInputAssemblyStateCreateInfo input_assembly_info { vk::PipelineInputAssemblyStateCreateFlags { }, vk::PrimitiveTopology::eTriangleStrip, false };
|
||||
|
||||
auto viewport = vk::Viewport { 0.f, 0.f, static_cast<float>(fb_width), static_cast<float>(fb_height), 0.f, 1.f };
|
||||
|
||||
vk::Rect2D scissor { vk::Offset2D { 0, 0 }, vk::Extent2D { fb_width, fb_height } };
|
||||
vk::PipelineViewportStateCreateInfo viewport_info { vk::PipelineViewportStateCreateFlags { }, 1, &viewport, 1, &scissor };
|
||||
|
||||
vk::PipelineRasterizationStateCreateInfo rasterization_info { vk::PipelineRasterizationStateCreateFlags { },
|
||||
false, // depth clamp enable
|
||||
false, // rasterizer discard enable
|
||||
vk::PolygonMode::eFill,
|
||||
vk::CullModeFlagBits::eNone,
|
||||
vk::FrontFace::eClockwise,
|
||||
false, // depth bias + 3 float parameters
|
||||
0.f,
|
||||
0.f,
|
||||
0.f,
|
||||
1.f // line width
|
||||
};
|
||||
|
||||
vk::PipelineMultisampleStateCreateInfo multisample_info { vk::PipelineMultisampleStateCreateFlags { },
|
||||
vk::SampleCountFlagBits::e1,
|
||||
false,
|
||||
1.0f
|
||||
};
|
||||
|
||||
vk::PipelineColorBlendAttachmentState attachment_blend_state {
|
||||
false, {}, {}, {}, {}, {}, {}, // No blending enabled
|
||||
vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA
|
||||
};
|
||||
|
||||
vk::PipelineDepthStencilStateCreateInfo depth_stencil_info { };
|
||||
|
||||
vk::PipelineColorBlendStateCreateInfo blend_info { vk::PipelineColorBlendStateCreateFlags { },
|
||||
false, {}, // logic op
|
||||
1, // attachment count
|
||||
&attachment_blend_state
|
||||
};
|
||||
|
||||
|
||||
vk::PipelineVertexInputStateCreateInfo vertex_input_info { };
|
||||
|
||||
vk::GraphicsPipelineCreateInfo info { vk::PipelineCreateFlags { },
|
||||
static_cast<uint32_t>(shader_infos.size()),
|
||||
shader_infos.data(),
|
||||
&vertex_input_info,
|
||||
&input_assembly_info,
|
||||
nullptr,
|
||||
&viewport_info,
|
||||
&rasterization_info,
|
||||
&multisample_info,
|
||||
&depth_stencil_info,
|
||||
&blend_info,
|
||||
nullptr,
|
||||
layout,
|
||||
renderpass,
|
||||
0
|
||||
};
|
||||
return device.createGraphicsPipelineUnique(vk::PipelineCache { }, info).value;
|
||||
}
|
||||
|
||||
EmuDisplay::EmuDisplay(spdlog::logger& logger, vk::PhysicalDevice physical_device, vk::Device device, uint32_t graphics_queue_index) {
|
||||
for (auto stream_id : { DataStreamId::TopScreenLeftEye, DataStreamId::TopScreenRightEye, DataStreamId::BottomScreen }) {
|
||||
auto& screen_part = screen_parts[Meta::to_underlying(stream_id)];
|
||||
retired_frames[Meta::to_underlying(stream_id)].push(&screen_part[0]);
|
||||
retired_frames[Meta::to_underlying(stream_id)].push(&screen_part[1]);
|
||||
}
|
||||
|
||||
vk::CommandPoolCreateInfo command_pool_info { vk::CommandPoolCreateFlagBits::eTransient | vk::CommandPoolCreateFlagBits::eResetCommandBuffer, graphics_queue_index };
|
||||
command_pool = device.createCommandPoolUnique(command_pool_info);
|
||||
|
||||
vk::DescriptorPoolSize pool_size {
|
||||
vk::DescriptorType::eCombinedImageSampler,
|
||||
static_cast<uint32_t>(screen_parts.size() * std::size(image_blitter)) // descriptor count
|
||||
};
|
||||
vk::DescriptorPoolCreateInfo pool_info {
|
||||
{},
|
||||
static_cast<uint32_t>(screen_parts.size() * std::size(image_blitter)), // number of sets
|
||||
1, // number of pool sizes
|
||||
&pool_size
|
||||
};
|
||||
desc_pool = device.createDescriptorPoolUnique(pool_info);
|
||||
|
||||
for (auto& stream_id : { DataStreamId::TopScreenLeftEye, DataStreamId::TopScreenRightEye, DataStreamId::BottomScreen }) {
|
||||
auto& image_blitter = this->image_blitter[Meta::to_underlying(stream_id)];
|
||||
|
||||
{
|
||||
std::vector<vk::AttachmentDescription> attachment_desc = {{
|
||||
vk::AttachmentDescriptionFlags { },
|
||||
image_blitter.target_format,
|
||||
vk::SampleCountFlagBits::e1,
|
||||
vk::AttachmentLoadOp::eDontCare,
|
||||
vk::AttachmentStoreOp::eStore,
|
||||
vk::AttachmentLoadOp::eDontCare,
|
||||
vk::AttachmentStoreOp::eDontCare,
|
||||
vk::ImageLayout::eUndefined,
|
||||
vk::ImageLayout::eGeneral
|
||||
}};
|
||||
|
||||
vk::AttachmentReference attachment_ref { 0, vk::ImageLayout::eGeneral };
|
||||
vk::SubpassDescription subpass_desc {
|
||||
vk::SubpassDescriptionFlags { },
|
||||
vk::PipelineBindPoint::eGraphics,
|
||||
0, nullptr, // Input attachments
|
||||
1, &attachment_ref, // color attachments
|
||||
nullptr, // resolve attachments
|
||||
nullptr, // depth stencil attachment
|
||||
0, nullptr // preserve attachments
|
||||
};
|
||||
vk::RenderPassCreateInfo renderpass_info { vk::RenderPassCreateFlags { }, static_cast<uint32_t>(attachment_desc.size()), attachment_desc.data(), 1, &subpass_desc, 0, nullptr };
|
||||
image_blitter.renderpass = device.createRenderPassUnique(renderpass_info);
|
||||
}
|
||||
|
||||
{
|
||||
vk::DescriptorSetLayoutBinding binding {
|
||||
0,
|
||||
vk::DescriptorType::eCombinedImageSampler,
|
||||
1,
|
||||
vk::ShaderStageFlagBits::eFragment,
|
||||
nullptr
|
||||
};
|
||||
|
||||
vk::DescriptorSetLayoutCreateInfo info {
|
||||
vk::DescriptorSetLayoutCreateFlags { },
|
||||
1, &binding
|
||||
};
|
||||
image_blitter.tex_binding = device.createDescriptorSetLayoutUnique(info);
|
||||
|
||||
vk::PipelineLayoutCreateInfo layout_info {
|
||||
vk::PipelineLayoutCreateFlags { },
|
||||
1, &*image_blitter.tex_binding, // descriptor set layouts
|
||||
0, nullptr, // push constant ranges
|
||||
};
|
||||
image_blitter.pipeline_layout = device.createPipelineLayoutUnique(layout_info);
|
||||
}
|
||||
|
||||
uint32_t width = (stream_id == DataStreamId::BottomScreen) ? 320 : 400;
|
||||
uint32_t height = 240;
|
||||
|
||||
auto& screen_frames = screen_parts[Meta::to_underlying(stream_id)];
|
||||
for (size_t buffer_index = 0; buffer_index < screen_frames.size(); ++buffer_index) {
|
||||
auto& image = screen_frames[buffer_index].image;
|
||||
|
||||
vk::ImageCreateInfo info {
|
||||
vk::ImageCreateFlags { },
|
||||
vk::ImageType::e2D,
|
||||
image_blitter.target_format, // TODO: Check format support. Any 8-bit RGB format will suffice
|
||||
vk::Extent3D { width, height, 1 },
|
||||
1,
|
||||
1,
|
||||
vk::SampleCountFlagBits::e1,
|
||||
vk::ImageTiling::eOptimal,
|
||||
vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eColorAttachment,
|
||||
vk::SharingMode::eExclusive, // TODO: Probably should be sharing this?
|
||||
0, nullptr, // queue families
|
||||
vk::ImageLayout::eUndefined
|
||||
};
|
||||
image = device.createImageUnique(info);
|
||||
Pica::Vulkan::SetDebugName(device, image, fmt::format("EmuDisplay image {} for screen {}", buffer_index, static_cast<int>(stream_id)).c_str());
|
||||
auto requirements = device.getImageMemoryRequirements(*image);
|
||||
|
||||
auto properties = physical_device.getMemoryProperties();
|
||||
for (size_t type_index = 0; type_index < properties.memoryTypeCount; ++type_index) {
|
||||
auto& type = properties.memoryTypes[type_index];
|
||||
auto& heap = properties.memoryHeaps[type.heapIndex];
|
||||
logger.info("Memory type {}: {}, using heap {} ({} bytes, {})", type_index, vk::to_string(type.propertyFlags), type.heapIndex, heap.size, vk::to_string(heap.flags));
|
||||
}
|
||||
|
||||
// TODO: Review which flags are needed
|
||||
// TODO: Observed sporadic crashes on startup when using this without eHostCoherent
|
||||
Pica::Vulkan::MemoryTypeDatabase memory_types { logger, physical_device };
|
||||
auto memory_info = memory_types.GetMemoryAllocateInfo<vk::MemoryPropertyFlagBits::eDeviceLocal>(requirements);
|
||||
screen_frames[buffer_index].buffer_memory = device.allocateMemoryUnique(memory_info);
|
||||
|
||||
device.bindImageMemory(*image, *screen_frames[buffer_index].buffer_memory, 0);
|
||||
|
||||
|
||||
|
||||
vk::ImageViewCreateInfo image_view_info {
|
||||
vk::ImageViewCreateFlags { },
|
||||
*image,
|
||||
vk::ImageViewType::e2D,
|
||||
image_blitter.target_format,
|
||||
vk::ComponentMapping { },
|
||||
vk::ImageSubresourceRange { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 }
|
||||
};
|
||||
screen_frames[buffer_index].image_view = device.createImageViewUnique(image_view_info);
|
||||
|
||||
vk::SamplerCreateInfo sampler_info {
|
||||
vk::SamplerCreateFlags { },
|
||||
vk::Filter::eNearest,
|
||||
vk::Filter::eNearest,
|
||||
vk::SamplerMipmapMode::eNearest,
|
||||
vk::SamplerAddressMode::eRepeat,
|
||||
vk::SamplerAddressMode::eRepeat,
|
||||
vk::SamplerAddressMode::eRepeat,
|
||||
0.f, // LOD bias
|
||||
false, // anisotropic filter
|
||||
0.f, // anisotropic filter
|
||||
false, // compare enable
|
||||
vk::CompareOp::eNever,
|
||||
0.f, // min LOD
|
||||
0.f, // max LOD
|
||||
vk::BorderColor::eFloatTransparentBlack,
|
||||
false // unnormalized coordinates
|
||||
};
|
||||
screen_frames[buffer_index].image_sampler = device.createSamplerUnique(sampler_info);
|
||||
|
||||
vk::FramebufferCreateInfo fb_info { vk::FramebufferCreateFlags { }, *image_blitter.renderpass, 1, &*screen_frames[buffer_index].image_view, width, height, 1 };
|
||||
screen_frames[buffer_index].framebuffer = device.createFramebufferUnique(fb_info);
|
||||
{
|
||||
vk::DescriptorSetAllocateInfo desc_set_info {
|
||||
*desc_pool,
|
||||
1,
|
||||
&*image_blitter.tex_binding
|
||||
};
|
||||
screen_frames[buffer_index].desc_set = std::move(device.allocateDescriptorSets(desc_set_info)[0]);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const char* code = "#version 450\n"
|
||||
"#extension GL_ARB_separate_shader_objects : enable\n"
|
||||
"\n"
|
||||
"void main() {"
|
||||
" if (gl_VertexIndex == 0) {\n"
|
||||
" gl_Position = vec4(-1.0, -1.0, 0.0, 1.0);\n"
|
||||
" } else if (gl_VertexIndex == 1) {\n"
|
||||
" gl_Position = vec4(-1.0, 1.0, 0.0, 1.0);\n"
|
||||
" } else if (gl_VertexIndex == 2) {\n"
|
||||
" gl_Position = vec4(1.0, -1.0, 0.0, 1.0);\n"
|
||||
" } else if (gl_VertexIndex == 3) {\n"
|
||||
" gl_Position = vec4(1.0, 1.0, 0.0, 1.0);\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
auto shader_spv = CompileShader(shaderc_glsl_vertex_shader, code, "main");
|
||||
|
||||
vk::ShaderModuleCreateInfo shader_module_info {
|
||||
vk::ShaderModuleCreateFlags { }, shader_spv.size() * sizeof(shader_spv.data()[0]), shader_spv.data()
|
||||
};
|
||||
image_blitter.vertex_shader = device.createShaderModuleUnique(shader_module_info);
|
||||
}
|
||||
|
||||
{
|
||||
std::string code = "#version 450\n"
|
||||
"#extension GL_ARB_separate_shader_objects : enable\n"
|
||||
"\n"
|
||||
"layout(binding = 0) uniform sampler2D sampler0;\n"
|
||||
"layout(location = 0) out vec4 out_color;\n"
|
||||
"\n"
|
||||
"void main() {\n"
|
||||
// NOTE: Alpha channel is set to 1.0 here because the Qt frontend requires this for overlay effects (blur, ...) to work
|
||||
" out_color = vec4(texture(sampler0, -gl_FragCoord.yx * vec2(" + std::to_string(1.0 / height) + ", " + std::to_string(1.0 / width) + ")).rgb, 1.0);\n"
|
||||
"}\n";
|
||||
auto shader_spv = CompileShader(shaderc_glsl_fragment_shader, code.c_str(), "main");
|
||||
|
||||
vk::ShaderModuleCreateInfo shader_module_info {
|
||||
vk::ShaderModuleCreateFlags { }, shader_spv.size() * sizeof(shader_spv.data()[0]), shader_spv.data()
|
||||
};
|
||||
image_blitter.fragment_shader = device.createShaderModuleUnique(shader_module_info);
|
||||
}
|
||||
|
||||
image_blitter.pipeline = CreateGraphicsPipelineForFullscreenEffect(width, height,
|
||||
device,
|
||||
*image_blitter.renderpass,
|
||||
*image_blitter.pipeline_layout,
|
||||
*image_blitter.vertex_shader,
|
||||
*image_blitter.fragment_shader);
|
||||
}
|
||||
}
|
||||
|
||||
EmuDisplay::~EmuDisplay() = default;
|
||||
|
||||
Frame& EmuDisplay::PrepareImage(DataStreamId stream_id, uint64_t timestamp) {
|
||||
Frame* frame = nullptr;
|
||||
while (retired_frames[Meta::to_underlying(stream_id)].pop(&frame, 1) != 1) {
|
||||
if (exit_requested) {
|
||||
fprintf(stderr, "Received exit notification from EmuDisplay in PrepareImage, unwinding...\n");
|
||||
throw ForceUnwind{};
|
||||
}
|
||||
|
||||
// TODO: Use a condition variable instead?
|
||||
// fprintf(stderr, "Waiting for next retired frame... %lu\n", timestamp);
|
||||
std::this_thread::yield();
|
||||
}
|
||||
return *frame;
|
||||
}
|
||||
|
||||
void EmuDisplay::PushImage(DataStreamId stream_id, Frame& frame, uint64_t timestamp) {
|
||||
frame.timestamp = timestamp;
|
||||
// fprintf(stderr, "Pushing frame: %lu\n", timestamp);
|
||||
auto pushed = incoming_frames[Meta::to_underlying(stream_id)].push(&frame);
|
||||
if (!pushed) {
|
||||
if (exit_requested) {
|
||||
fprintf(stderr, "Received exit notification from EmuDisplay in PushImage, unwinding...\n");
|
||||
throw ForceUnwind{};
|
||||
}
|
||||
throw std::runtime_error("QUEUE FULL");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void EmuDisplay::SeekTo(uint64_t timestamp) {
|
||||
// fprintf(stderr, "Seeking to %lu\n", timestamp);
|
||||
for (auto stream_id : { DataStreamId::TopScreenLeftEye, DataStreamId::TopScreenRightEye, DataStreamId::BottomScreen }) {
|
||||
// Pop entries from incoming_frames to ready_frames
|
||||
auto& incoming = incoming_frames[Meta::to_underlying(stream_id)];
|
||||
auto& ready = ready_frames[Meta::to_underlying(stream_id)];
|
||||
while (incoming.read_available()) {
|
||||
ready.push_back(incoming.front());
|
||||
incoming.pop();
|
||||
}
|
||||
// fprintf(stderr, "Ready queue elements: %d\n", (int)ready.size());
|
||||
|
||||
// Scenarios to handle:
|
||||
// * Emulator running too slow => seek time is always newer than timestamps
|
||||
// * Images should display as soon as possible, if need be separately from each other
|
||||
// * Emulator running too fast, with speed limit => seek time is always behind timestamps
|
||||
// * Each image should be displayed at least once
|
||||
// * Emulator running too fast, no speed limit => seek time is always behind timestamps
|
||||
// * Skip images
|
||||
|
||||
for (auto frame_it = ready.begin(); frame_it != ready.end();) {
|
||||
Frame* frame = *frame_it;
|
||||
// Drop outdated frames if newer ones are queued already
|
||||
if (frame->timestamp < timestamp && ready.size() > 1) {
|
||||
fprintf(stderr, "Discarding frame %lu\n", (**frame_it).timestamp);
|
||||
frame_it = ready.erase(frame_it);
|
||||
retired_frames[Meta::to_underlying(stream_id)].push(frame);
|
||||
continue;
|
||||
} else if (frame->timestamp < timestamp + 1000 && ready.size() == 1) {
|
||||
// TODO: Consider this stream inactive, since no new frame has been pushed in a while
|
||||
}
|
||||
|
||||
++frame_it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Frame& EmuDisplay::GetCurrent(DataStreamId stream_id) {
|
||||
return *ready_frames[Meta::to_underlying(stream_id)].front();
|
||||
}
|
||||
|
||||
} // namespace Display
|
142
source/display.hpp
Normal file
142
source/display.hpp
Normal file
|
@ -0,0 +1,142 @@
|
|||
#pragma once
|
||||
|
||||
#include "framework/image_format.hpp"
|
||||
|
||||
#include <boost/lockfree/spsc_queue.hpp>
|
||||
|
||||
#include <array>
|
||||
|
||||
#include <vulkan/vulkan.hpp>
|
||||
|
||||
namespace spdlog {
|
||||
class logger;
|
||||
}
|
||||
|
||||
namespace EmuDisplay {
|
||||
|
||||
enum class DataStreamId {
|
||||
TopScreenLeftEye, // or top screen image when 3D is disabled
|
||||
TopScreenRightEye,
|
||||
BottomScreen
|
||||
};
|
||||
|
||||
struct Format {
|
||||
uint32_t raw;
|
||||
|
||||
static constexpr std::array<GenericImageFormat, 5> format_map = {{
|
||||
GenericImageFormat::RGBA8,
|
||||
GenericImageFormat::RGB8,
|
||||
GenericImageFormat::RGB565,
|
||||
GenericImageFormat::RGBA5551,
|
||||
GenericImageFormat::RGBA4
|
||||
}};
|
||||
};
|
||||
|
||||
struct Frame {
|
||||
vk::UniqueImage image;
|
||||
vk::UniqueImageView image_view;
|
||||
vk::UniqueSampler image_sampler;
|
||||
vk::UniqueFramebuffer framebuffer;
|
||||
vk::UniqueDeviceMemory buffer_memory;
|
||||
vk::DescriptorSet desc_set; // Descriptor sets for source image copied onto "image"
|
||||
|
||||
vk::UniqueCommandBuffer command_buffer;
|
||||
// vk::UniqueSemaphore buffer_ready_semaphore;
|
||||
|
||||
uint64_t timestamp;
|
||||
};
|
||||
|
||||
// TODO: Add custom timestamp type that is robust against overflows when comparing
|
||||
|
||||
struct EmuDisplay {
|
||||
struct ForceUnwind {};
|
||||
|
||||
EmuDisplay(spdlog::logger&, vk::PhysicalDevice, vk::Device, uint32_t graphics_queue_index);
|
||||
|
||||
virtual ~EmuDisplay();
|
||||
|
||||
// TODO: Drop need for this interface
|
||||
void ClearResources() {
|
||||
image_blitter[0] = {};
|
||||
image_blitter[1] = {};
|
||||
image_blitter[2] = {};
|
||||
|
||||
screen_parts[0][0] = {};
|
||||
screen_parts[0][1] = {};
|
||||
screen_parts[1][0] = {};
|
||||
screen_parts[1][1] = {};
|
||||
screen_parts[2][0] = {};
|
||||
screen_parts[2][1] = {};
|
||||
|
||||
desc_pool = {};
|
||||
command_pool = {};
|
||||
}
|
||||
|
||||
vk::UniqueCommandPool command_pool;
|
||||
vk::UniqueDescriptorPool desc_pool;
|
||||
|
||||
// One for each stream
|
||||
struct ImageBlitter {
|
||||
vk::UniqueRenderPass renderpass {};
|
||||
vk::UniquePipelineLayout pipeline_layout {};
|
||||
vk::UniquePipeline pipeline {};
|
||||
vk::UniqueDescriptorSetLayout tex_binding {};
|
||||
vk::UniqueShaderModule vertex_shader {};
|
||||
vk::UniqueShaderModule fragment_shader {};
|
||||
|
||||
static constexpr vk::Format target_format = vk::Format::eR8G8B8A8Unorm;
|
||||
} image_blitter[3];
|
||||
|
||||
static constexpr int frames_in_flight = 2;
|
||||
|
||||
// For each stream, store 2 images
|
||||
std::array<std::array<Frame, frames_in_flight>, 3> screen_parts;
|
||||
|
||||
// Frames ready to be re-filled by renderer
|
||||
std::array<boost::lockfree::spsc_queue<Frame*, boost::lockfree::capacity<frames_in_flight>>, 3> retired_frames;
|
||||
|
||||
// Frames ready to be used by consumer (to be moved to ready_frames)
|
||||
std::array<boost::lockfree::spsc_queue<Frame*, boost::lockfree::capacity<frames_in_flight>>, 3> incoming_frames;
|
||||
|
||||
// Frames ready for use by consumer, sorted by increasing timestamp
|
||||
std::array<std::vector<Frame*>, 3> ready_frames;
|
||||
|
||||
// Set to true by consumer on exit. Renderer should check for this while waiting for retired frames to avoid deadlocks
|
||||
// TODO: Add public interface for this, ideally a RAII based client class
|
||||
std::atomic<bool> exit_requested = false;
|
||||
|
||||
// Renderer interface
|
||||
|
||||
Frame& PrepareImage(DataStreamId stream_id, uint64_t timestamp);
|
||||
|
||||
void PushImage(DataStreamId stream_id, Frame& frame, uint64_t timestamp);
|
||||
|
||||
// Consumer interface
|
||||
|
||||
/**
|
||||
* Update the current frame set selection for the given target timestamp.
|
||||
* This is the set of two top screen images and one bottom screen image
|
||||
* that the frontend should display in the next host frame.
|
||||
*
|
||||
* The ideal frame set is characterized by a number of criteria:
|
||||
* - The timestamp of each image should be close to the target timestamp
|
||||
* - Rendered screen images should not be displayed out-of-sync, so the timestamps of the images should match up closely among themselves
|
||||
* - Each screen image should be displayed at least once
|
||||
* - The host display should update at a regular interval even if emulation speed fluctuates slightly
|
||||
*
|
||||
* This function uses heuristics to balance these criteria against each
|
||||
* other. Frames that won't be used anymore are retired, which allows them
|
||||
* to be used for new images by the renderer.
|
||||
*/
|
||||
void SeekTo(uint64_t timestamp);
|
||||
|
||||
/**
|
||||
* Update the current frame set selection for the given target timestamp.
|
||||
*
|
||||
* This frame is selected based on internal heuristics (see SeekTo for
|
||||
* details).
|
||||
*/
|
||||
Frame& GetCurrent(DataStreamId stream_id);
|
||||
};
|
||||
|
||||
} // namespace EmuDisplay
|
10
source/framework/CMakeLists.txt
Normal file
10
source/framework/CMakeLists.txt
Normal file
|
@ -0,0 +1,10 @@
|
|||
# Adding an interface library to propagate include directories
|
||||
add_library(framework exceptions.cpp profiler.cpp)
|
||||
target_include_directories(framework INTERFACE $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/..>)
|
||||
|
||||
target_link_libraries(framework PUBLIC fmt::fmt)
|
||||
target_link_libraries(framework INTERFACE range-v3::range-v3)
|
||||
target_link_libraries(framework PUBLIC Boost::boost)
|
||||
if(libunwind_FOUND)
|
||||
target_link_libraries(framework PRIVATE libunwind::unwind)
|
||||
endif ()
|
374
source/framework/bit_field_new.hpp
Normal file
374
source/framework/bit_field_new.hpp
Normal file
|
@ -0,0 +1,374 @@
|
|||
#pragma once
|
||||
|
||||
#include <climits>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
namespace v2 {
|
||||
|
||||
namespace BitField {
|
||||
|
||||
template<typename>
|
||||
struct Fields;
|
||||
|
||||
namespace detail {
|
||||
|
||||
// TODO: Consider adding a policy parameter; a candidate for a policy is bounds-checking and wrapping/clamping the argument to Set()
|
||||
template<std::size_t Position, std::size_t Bits, typename ParentClass, typename Storage, typename Exposure = Storage>
|
||||
class Field {
|
||||
friend struct Fields<Storage>;
|
||||
|
||||
ParentClass object;
|
||||
|
||||
constexpr Field(ParentClass object) : object(object) {}
|
||||
|
||||
// Store intermediate computations as integral_constants to make sure even the dumbest compiler can inline our member functions
|
||||
// NOTE: the double Storage cast in full_mask is intended: E.g. if Storage is uint64_t and assuming 0 is of type int32_t, ~0 wouldn't match the expected 0xffffffffffffffff. On the other hand, if Storage is unsigned char, ~(Storage)0 gets promoted to the int value -1.
|
||||
static constexpr auto full_mask = std::integral_constant<Storage, static_cast<Storage>(~Storage{0})>::value;
|
||||
static constexpr auto field_mask = std::integral_constant<Storage, ((full_mask >> (8 * sizeof(Storage) - Bits)) << Position)>::value;
|
||||
static constexpr auto neg_field_mask = std::integral_constant<Storage, static_cast<Storage>(~field_mask)>::value;
|
||||
|
||||
public:
|
||||
operator Exposure() const {
|
||||
return static_cast<Exposure>((object.storage & field_mask) >> Position);
|
||||
}
|
||||
|
||||
// Setter: Copies the storage of the parent instance and returns the manipulated value (contained in the new parent instance)
|
||||
ParentClass Set(Exposure value) const {
|
||||
//static_assert(std::is_base_of_v<BitsOnBase, ParentClass>, "Given ParentClass does not inherit BitsOnBase");
|
||||
static_assert(sizeof(ParentClass) == sizeof(Storage), "ParentClass has additional members other than storage");
|
||||
|
||||
auto ret = object;
|
||||
ret.storage = (object.storage & neg_field_mask) | ((static_cast<Storage>(value) << Position) & field_mask);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// Concise version of casting to Exposure
|
||||
Exposure operator ()() const {
|
||||
return static_cast<Exposure>(*this);
|
||||
}
|
||||
|
||||
/// Concise version of Set(value)
|
||||
ParentClass operator ()(Exposure value) const {
|
||||
return Set(value);
|
||||
}
|
||||
|
||||
static constexpr Exposure MaxValue() {
|
||||
// TODO: This may overflow (e.g. Bits=32, Exposure=uint32_t)
|
||||
return static_cast<Exposure>((Storage{1} << Bits) - 1);
|
||||
}
|
||||
|
||||
// TODO: SignedBitField
|
||||
};
|
||||
|
||||
// Base class defining the Field substruct. Expects ParentClass to have a single (inherited or non-inherited) member called "storage" of unsigned type (custom arithmetic allowed, howver, e.g. to implement endianness conversion).
|
||||
template<typename Storage, typename ParentClass>
|
||||
struct BitsOnBase {
|
||||
template<std::size_t Pos, std::size_t Bits, typename Exposure = Storage>
|
||||
auto MakeField() const {
|
||||
return Fields<Storage>{}.Make();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/// BitFields on a given Storage type
|
||||
/// TODO: Consider refactoring MakeOn to be a static constexpr function object rather than a function
|
||||
template<typename Storage>
|
||||
struct Fields {
|
||||
template<std::size_t Pos, std::size_t Bits, typename ParentClass, typename Exposure = Storage>
|
||||
static auto MakeOn(ParentClass obj) {
|
||||
return detail::Field<Pos, Bits, ParentClass, Storage, Exposure>{ obj };
|
||||
}
|
||||
|
||||
/// Convenience overload to allow directly passing in a this pointer
|
||||
template<std::size_t Pos, std::size_t Bits, typename ParentClass, typename Exposure = Storage>
|
||||
static auto MakeOn(const ParentClass* obj) {
|
||||
return detail::Field<Pos, Bits, ParentClass, Storage, Exposure>{ *obj };
|
||||
}
|
||||
|
||||
/// TODO: Privatize
|
||||
template<std::size_t Pos, std::size_t Bits, typename Exposure>
|
||||
struct MakeOn2Type {
|
||||
template<typename ParentClass>
|
||||
constexpr auto operator()(ParentClass obj) const {
|
||||
return detail::Field<Pos, Bits, ParentClass, Storage, Exposure>{ obj };
|
||||
}
|
||||
};
|
||||
|
||||
template<std::size_t Pos, std::size_t Bits, typename Exposure = Storage>
|
||||
static constexpr auto MakeOn2 = MakeOn2Type<Pos, Bits, Exposure>{};
|
||||
};
|
||||
|
||||
|
||||
struct OwnStoragePolicy {};
|
||||
|
||||
/**
|
||||
* Convenience class to allow for terser syntax and to automatially allocate storage.
|
||||
* Child classes only define the actual fields.
|
||||
*/
|
||||
template<typename Storage, typename ParentClass, typename Policy = OwnStoragePolicy>
|
||||
struct BitsOn : detail::BitsOnBase<Storage, ParentClass> {
|
||||
// Rely on ParentClass providing a "storage" member unless ProvideStorage is true (see specialization below)
|
||||
};
|
||||
|
||||
template<typename Storage, typename ParentClass>
|
||||
struct BitsOn<Storage, ParentClass, OwnStoragePolicy> : detail::BitsOnBase<Storage, ParentClass> {
|
||||
// Automatically allocate the "storage" member
|
||||
Storage storage;
|
||||
};
|
||||
|
||||
} // namespace BitField
|
||||
|
||||
} // namespace v2
|
||||
|
||||
namespace v3 {
|
||||
|
||||
namespace BitField {
|
||||
|
||||
namespace detail {
|
||||
|
||||
template<std::size_t Bits>
|
||||
using uint_least_n_bits =
|
||||
std::conditional_t<Bits <= sizeof(std::uint_least8_t) * CHAR_BIT, std::uint_least8_t,
|
||||
std::conditional_t<Bits <= sizeof(std::uint_least16_t) * CHAR_BIT, std::uint_least16_t,
|
||||
std::conditional_t<Bits <= sizeof(std::uint_least32_t) * CHAR_BIT, std::uint_least32_t,
|
||||
std::conditional_t<Bits <= sizeof(std::uint_least64_t) * CHAR_BIT, std::uint_least64_t,
|
||||
std::uintmax_t>>>>;
|
||||
|
||||
template<std::size_t Position, std::size_t Bits, typename ParentClass, typename Storage, typename Exposure = Storage>
|
||||
class Field {
|
||||
public:
|
||||
ParentClass object;
|
||||
|
||||
constexpr Field(ParentClass object) : object(object) {}
|
||||
|
||||
// Store intermediate computations as integral_constants to make sure even the dumbest compiler can inline our member functions
|
||||
// NOTE: the double Storage cast in full_mask is intended: E.g. if Storage is uint64_t and assuming 0 is of type int32_t, ~0 wouldn't match the expected 0xffffffffffffffff. On the other hand, if Storage is unsigned char, ~(Storage)0 gets promoted to the int value -1.
|
||||
static constexpr auto full_mask = std::integral_constant<Storage, static_cast<Storage>(~Storage{0})>::value;
|
||||
static constexpr auto field_mask = std::integral_constant<Storage, ((full_mask >> (8 * sizeof(Storage) - Bits)) << Position)>::value;
|
||||
static constexpr auto neg_field_mask = std::integral_constant<Storage, static_cast<Storage>(~field_mask)>::value;
|
||||
|
||||
// TODO: static_assert the Exposure type can store up to Bits bits. Refer to "operator Exposure()" to see how to distinguish the various cases
|
||||
|
||||
public:
|
||||
constexpr operator Exposure() const {
|
||||
// Extract the bit field value and convert it to Exposure. The latter step is trivial for scalar Exposures, but can be tricky for class-types:
|
||||
// * For aggregates, assign through a structured binding (because brace-initialization could be narrowing)
|
||||
|
||||
if constexpr (std::is_class_v<ParentClass>) {
|
||||
auto [storage] = object;
|
||||
if constexpr (std::is_scalar_v<Exposure>) {
|
||||
return static_cast<Exposure>((storage & field_mask) >> Position);
|
||||
} else {
|
||||
// * use aggregate-initialization for aggregate types, or
|
||||
// * call single-argument constructor for non-aggregate types
|
||||
//
|
||||
// In both cases, the result type of the bit field extraction
|
||||
// may be larger than the aggregate member or constructor
|
||||
// argument. To prevent narrowing warnings in this case, we
|
||||
// static_cast the intermediate value to the smallest integer
|
||||
// that can hold Bits bits
|
||||
|
||||
// TODO: How well will this work with signed storages?
|
||||
|
||||
// TODO: Consider adding a customization point for this conversion
|
||||
return Exposure { static_cast<uint_least_n_bits<Bits>>((storage & field_mask) >> Position) };
|
||||
}
|
||||
} else {
|
||||
auto storage = object;
|
||||
// return Exposure { (storage & field_mask) >> Position };
|
||||
return static_cast<Exposure>((storage & field_mask) >> Position);
|
||||
}
|
||||
}
|
||||
|
||||
// Setter: Copies the storage of the parent instance and returns the manipulated value (contained in the new parent instance)
|
||||
constexpr ParentClass Set(Exposure value) const {
|
||||
//static_assert(std::is_base_of_v<BitsOnBase, ParentClass>, "Given ParentClass does not inherit BitsOnBase");
|
||||
static_assert(sizeof(ParentClass) == sizeof(Storage), "ParentClass has additional members other than storage");
|
||||
|
||||
auto ret = object;
|
||||
if constexpr (std::is_class_v<ParentClass>) {
|
||||
auto& [storage] = ret;
|
||||
storage = (storage & neg_field_mask) | ((static_cast<Storage>(value) << Position) & field_mask);
|
||||
} else {
|
||||
auto storage = object;
|
||||
ret = (storage & neg_field_mask) | ((static_cast<Storage>(value) << Position) & field_mask);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// Concise version of casting to Exposure
|
||||
constexpr Exposure operator ()() const {
|
||||
return static_cast<Exposure>(*this);
|
||||
}
|
||||
|
||||
/// Concise version of Set(value)
|
||||
constexpr ParentClass operator ()(Exposure value) const {
|
||||
return Set(value);
|
||||
}
|
||||
|
||||
static constexpr Exposure MaxValue() {
|
||||
return Exposure { (Storage{1} << Bits) - 1 };
|
||||
}
|
||||
|
||||
static constexpr std::size_t NumBits() noexcept {
|
||||
return Bits;
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
constexpr auto GetStorage(const T& t) {
|
||||
auto& [storage] = t;
|
||||
return storage;
|
||||
}
|
||||
|
||||
// TODO: Instead of using a PointerToMember here, a generic lens would allow for accessing e.g. arrays!
|
||||
// TODO: Define generic Lens concept: Must be a callable with signature Class -> (Member -> Member) -> (Member, Class)
|
||||
template<size_t Position, size_t Bits, typename ParentClass, typename MemberLens,
|
||||
typename Exposure = typename MemberLens::MemberType>
|
||||
class FieldNew {
|
||||
using Storage = typename MemberLens::MemberType;
|
||||
|
||||
public:
|
||||
ParentClass object;
|
||||
|
||||
constexpr FieldNew(ParentClass object) : object(object) {}
|
||||
|
||||
// Store intermediate computations as integral_constants to make sure even the dumbest compiler can inline our member functions
|
||||
// NOTE: the double Storage cast in full_mask is intended: E.g. if Storage is uint64_t and assuming 0 is of type int32_t, ~0 wouldn't match the expected 0xffffffffffffffff. On the other hand, if Storage is unsigned char, ~(Storage)0 gets promoted to the int value -1.
|
||||
static constexpr auto full_mask = std::integral_constant<Storage, static_cast<Storage>(~Storage{0})>::value;
|
||||
static constexpr auto field_mask = std::integral_constant<Storage, ((full_mask >> (8 * sizeof(Storage) - Bits)) << Position)>::value;
|
||||
static constexpr auto neg_field_mask = std::integral_constant<Storage, static_cast<Storage>(~field_mask)>::value;
|
||||
|
||||
public:
|
||||
constexpr operator Exposure() const {
|
||||
return static_cast<Exposure>((MemberLens::get(object) & field_mask) >> Position);
|
||||
}
|
||||
|
||||
// Setter: Copies the storage of the parent instance and returns the manipulated value (contained in the new parent instance)
|
||||
ParentClass Set(Exposure value) const {
|
||||
return MemberLens::modify( *this,
|
||||
[&](const auto& data) { return (data & neg_field_mask) | ((static_cast<Storage>(value) << Position) & field_mask); });
|
||||
}
|
||||
|
||||
/// Concise version of casting to Exposure
|
||||
constexpr Exposure operator ()() const {
|
||||
return static_cast<Exposure>(*this);
|
||||
}
|
||||
|
||||
/// Concise version of Set(value)
|
||||
constexpr ParentClass operator ()(Exposure value) const {
|
||||
return Set(value);
|
||||
}
|
||||
|
||||
static constexpr Exposure MaxValue() {
|
||||
// TODO: This may overflow (e.g. Bits=32, Exposure=uint32_t)
|
||||
return static_cast<Exposure>((Storage{1} << Bits) - 1);
|
||||
}
|
||||
|
||||
// TODO: SignedBitField
|
||||
};
|
||||
|
||||
template<auto PointerToMember>
|
||||
struct PMDLens;
|
||||
|
||||
template<typename Member, typename Class, Member Class::*PointerToMember>
|
||||
struct PMDLens<PointerToMember> {
|
||||
using MemberType = Member;
|
||||
|
||||
template<typename Modifier>
|
||||
static Class modify(Class&& object, Modifier&& modifier) {
|
||||
// TODO: Should avoid copy for rvalue objects
|
||||
auto ret = std::forward<Class>(object);
|
||||
ret.*PointerToMember = std::forward<Modifier>(modifier)(ret.*PointerToMember);
|
||||
return ret;
|
||||
}
|
||||
|
||||
template<typename Modifier>
|
||||
static Class modify(const Class& object, Modifier&& modifier) {
|
||||
auto ret = object;
|
||||
ret.*PointerToMember = std::forward<Modifier>(modifier)(ret.*PointerToMember);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static MemberType get(Class&& object) {
|
||||
return std::forward<Class>(object).*PointerToMember;
|
||||
}
|
||||
|
||||
static MemberType get(const Class& object) {
|
||||
return object.*PointerToMember;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
template<std::size_t Position, std::size_t Bits, typename Exposure, typename ParentClass>
|
||||
constexpr auto MakeFieldOn(const ParentClass* t) {
|
||||
if constexpr (std::is_class_v<ParentClass>) {
|
||||
using Storage = decltype(detail::GetStorage(std::declval<ParentClass>()));
|
||||
return detail::Field<Position, Bits, ParentClass, Storage, Exposure> { *t };
|
||||
} else {
|
||||
return detail::Field<Position, Bits, ParentClass, ParentClass, Exposure> { *t };
|
||||
}
|
||||
}
|
||||
|
||||
template<std::size_t Position, std::size_t Bits, typename ParentClass>
|
||||
constexpr auto MakeFieldOn(const ParentClass* t) {
|
||||
if constexpr (std::is_class_v<ParentClass>) {
|
||||
using Storage = decltype(detail::GetStorage(std::declval<ParentClass>()));
|
||||
return detail::Field<Position, Bits, ParentClass, Storage, Storage> { *t };
|
||||
} else {
|
||||
return detail::Field<Position, Bits, ParentClass, ParentClass, ParentClass> { *t };
|
||||
}
|
||||
}
|
||||
|
||||
// Usage, e.g.: auto enabled() const { return BitField::v3::MakeFlagOn< 0, &StencilTest::raw1>(this); }
|
||||
template<auto member, size_t Position, size_t Bits, typename Exposure = typename detail::PMDLens<member>::MemberType, typename ParentClass>
|
||||
constexpr detail::FieldNew<Position, Bits, ParentClass, detail::PMDLens<member>, Exposure>
|
||||
MakeFieldOn(const ParentClass* t) {
|
||||
static_assert(std::is_class_v<ParentClass>, "Must only call this overload on class types");
|
||||
static_assert(std::is_member_pointer_v<decltype(member)>, "Given member must be a pointer-to-member");
|
||||
static_assert(std::is_invocable_v<decltype(member), ParentClass>, "Given object type is not compatible with the pointer-to-member");
|
||||
|
||||
return { *t };
|
||||
}
|
||||
|
||||
template<auto member, size_t Position, typename ParentClass>
|
||||
constexpr detail::FieldNew<Position, 1, ParentClass, detail::PMDLens<member>, bool>
|
||||
MakeFlagOn(const ParentClass* t) {
|
||||
static_assert(std::is_class_v<ParentClass>, "Must only call this overload on class types");
|
||||
static_assert(std::is_member_pointer_v<decltype(member)>, "Given member must be a pointer-to-member");
|
||||
static_assert(std::is_invocable_v<decltype(member), ParentClass>, "Given object type is not compatible with the pointer-to-member");
|
||||
|
||||
return { *t };
|
||||
}
|
||||
|
||||
|
||||
template<std::size_t Position, typename ParentClass>
|
||||
constexpr auto MakeFlagOn(const ParentClass* t) {
|
||||
if constexpr (std::is_class_v<ParentClass>) {
|
||||
using Storage = decltype(detail::GetStorage(std::declval<ParentClass>()));
|
||||
return detail::Field<Position, 1, ParentClass, Storage, bool> { *t };
|
||||
} else {
|
||||
return detail::Field<Position, 1, ParentClass, ParentClass, bool> { *t };
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace BitField
|
||||
|
||||
} // namespace v3
|
||||
|
||||
namespace BitField {
|
||||
|
||||
namespace v2 {
|
||||
using namespace ::v2::BitField;
|
||||
}
|
||||
|
||||
namespace v3 {
|
||||
using namespace ::v3::BitField;
|
||||
}
|
||||
|
||||
}
|
27
source/framework/color.hpp
Normal file
27
source/framework/color.hpp
Normal file
|
@ -0,0 +1,27 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Color {
|
||||
|
||||
/// Convert a 1-bit color component to 8 bit
|
||||
static inline uint8_t Convert1To8(uint8_t value) {
|
||||
return value * 255;
|
||||
}
|
||||
|
||||
/// Convert a 4-bit color component to 8 bit
|
||||
static inline uint8_t Convert4To8(uint8_t value) {
|
||||
return (value << 4) | value;
|
||||
}
|
||||
|
||||
/// Convert a 5-bit color component to 8 bit
|
||||
static inline uint8_t Convert5To8(uint8_t value) {
|
||||
return (value << 3) | (value >> 2);
|
||||
}
|
||||
|
||||
/// Convert a 6-bit color component to 8 bit
|
||||
static inline uint8_t Convert6To8(uint8_t value) {
|
||||
return (value << 2) | (value >> 4);
|
||||
}
|
||||
|
||||
} // namespace
|
111
source/framework/config_framework.hpp
Normal file
111
source/framework/config_framework.hpp
Normal file
|
@ -0,0 +1,111 @@
|
|||
#pragma once
|
||||
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
namespace Config {
|
||||
|
||||
/**
|
||||
* \page config Configuration Framework
|
||||
*
|
||||
* Provides frontend-agnostic access to configuration.
|
||||
* Frontend code may be written such that it is ensured that all options are
|
||||
* handled properly at compile-time. This design keeps out issues of
|
||||
* representation out of the main configuration provider. Things like parsing
|
||||
* of options and stringifying configuration are purely frontend matters
|
||||
* (especially since the frontend is the one dictating the medium and format
|
||||
* for configuration storage).
|
||||
*
|
||||
* @todo Add support for gathering options into groups
|
||||
* @todo Allow encoding some sort of "cannot be changed during emulation" property into option tags
|
||||
* @todo Add a signalling mechanism for emulator subsystems to listen for configuration changes
|
||||
* @todo Support arrays of an option
|
||||
* @todo Evaluate whether adding support for option tags referring to incomplete storage types is feasible. This may be desirable for options that are set according to enumerations dictated by the emulated platform
|
||||
*
|
||||
* \section config_option_tags Option Tags
|
||||
*
|
||||
* Instead of having a structure that stores and defines all configuration
|
||||
* entries along with their current value, we explicitly define the set of
|
||||
* configuration entries using option tags, while the actual configuration
|
||||
* storage is handled separately.
|
||||
*
|
||||
* A set of configuration entries is defined by passing a set of option tags
|
||||
* to the \ref Options template. Option tags are structures inheriting the
|
||||
* \ref Option structure (or one of its child classes provided for convenience)
|
||||
* and need to provide a number of properties to define the configuration
|
||||
* entry:
|
||||
* - a type alias named \c type defining the data type used to represent the entry value
|
||||
* - a static member-function named \c default_value that returns the default value of this entry
|
||||
* - a static C-string member named \c name specifying a human-readable identifier for this entry
|
||||
*
|
||||
* \section config_rationale Design Rationale
|
||||
*
|
||||
* Encoding default options into option tags guarantees that the internal
|
||||
* configuration storage object is always in a valid state (as opposed to being
|
||||
* created in some sort of "uninitialized" state). It also reduced duplicate
|
||||
* frontend code, since frontends can just use the default configuration rather
|
||||
* than setting it up all by themselves (in particular, the CLI backend doesn't
|
||||
* really care about the particular configuration used, anyway). On the
|
||||
* negative side, changes to the defaults in the backend may be unexpected to
|
||||
* some frontents. Furthermore, frontends need to watch out not to forget
|
||||
* adjusting their code to consider the addition of new options. For these two
|
||||
* reasons, frontends are advised to either just accept the default settings
|
||||
* and stack individual modifications on top of them or to instead override
|
||||
* all of the settings with own ones (in a way that statically ensures all
|
||||
* options are covered).
|
||||
*/
|
||||
|
||||
/// Base option tag. All custom tags need to inherit this structure.
|
||||
struct Option { Option() = delete; };
|
||||
|
||||
template<typename Type, typename Tag = Type>
|
||||
struct OptionDefault : Option { using type = Type; static type default_val; static type default_value() { return default_val; } };
|
||||
|
||||
/// Convenience option structure to serve as a base for option tags representing boolean switches. default_value() must be defined by the user
|
||||
/// Uses CRTP so that each instance of this type is different, so that default_value can be implemented for each setting separately
|
||||
/// the member function default_value is merely provided for backwards compatibility reasons
|
||||
/// TODO: Clean up docstring
|
||||
template<typename CRTP>
|
||||
struct BooleanOption : Option { using type = bool; static type default_val; static type default_value() { return default_val; } };
|
||||
|
||||
/// Convenience option structure to serve as a base for option tags representing integer-based settings with a given default value.
|
||||
/// TODO: Clean up docstring
|
||||
template<typename IntType, typename CRTP>
|
||||
struct IntegralOption : Option { using type = IntType; static type default_val; static type default_value() { return default_val; } };
|
||||
|
||||
namespace detail {
|
||||
|
||||
// Helper type for easy lookup of a tagged element in a std::tuple
|
||||
template<typename Tag, typename Data>
|
||||
struct TaggedData {
|
||||
Data data;
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/**
|
||||
* Template class defining and storing a set of options in a type-safe manner
|
||||
*/
|
||||
template<typename... Tags>
|
||||
struct Options {
|
||||
using tags = std::tuple<Tags...>;
|
||||
|
||||
using storage_type = std::tuple<detail::TaggedData<Tags, typename Tags::type>...>;
|
||||
storage_type storage { { Tags::default_value() }... };
|
||||
|
||||
/// Get the value corresponding to the given option tag
|
||||
template<typename T>
|
||||
const typename T::type& get() const {
|
||||
static_assert(std::is_base_of_v<Option, T>, "Given type is not an option tag");
|
||||
return std::get<detail::TaggedData<T, typename T::type>>(storage).data;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void set(const typename T::type& data) {
|
||||
static_assert(std::is_base_of_v<Option, T>, "Given type is not an option tag");
|
||||
std::get<detail::TaggedData<T, typename T::type>>(storage).data = data;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Config
|
86
source/framework/console.cpp
Normal file
86
source/framework/console.cpp
Normal file
|
@ -0,0 +1,86 @@
|
|||
#include "console.hpp"
|
||||
#include "utility/simple_tcp.hpp"
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
|
||||
#include <range/v3/algorithm/find.hpp>
|
||||
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
|
||||
std::optional<std::string> Console::HandleCommand(const std::string& command) {
|
||||
const char* syntax_error_reply = "Syntax: <module>.<command> [<parameters>]";
|
||||
|
||||
auto dot_it = ranges::find(command, '.');
|
||||
if (dot_it == command.end())
|
||||
return {{syntax_error_reply}};
|
||||
|
||||
std::string desired_module_name{command.begin(), dot_it};
|
||||
|
||||
// Find a module matching the prefix
|
||||
for (auto& module : modules) {
|
||||
if (module.first != desired_module_name)
|
||||
continue;
|
||||
|
||||
std::string sub_command{dot_it + 1, command.end()};
|
||||
// TODO: command may include a newline character, so this check is pretty much useless currently
|
||||
if (sub_command.length() == 0)
|
||||
return {{syntax_error_reply}};
|
||||
|
||||
return module.second->HandleCommand(sub_command);
|
||||
}
|
||||
|
||||
return "No module called \"" + desired_module_name + "\" registered";
|
||||
}
|
||||
|
||||
void Console::RegisterModule(const std::string& name, std::unique_ptr<ConsoleModule> module) {
|
||||
if (modules.count(name))
|
||||
throw std::runtime_error("Module \"" + name + "\" already registered");
|
||||
|
||||
modules[name] = std::move(module);
|
||||
}
|
||||
|
||||
class NetworkConsoleServer : public SimpleTCPServer {
|
||||
Console& console;
|
||||
|
||||
boost::asio::streambuf streambuf {};
|
||||
|
||||
std::optional<boost::asio::ip::tcp::socket> client;
|
||||
|
||||
void OnClientConnected(boost::asio::ip::tcp::socket socket) override {
|
||||
client = std::move(socket);
|
||||
SetupLineReceivedHandler();
|
||||
}
|
||||
|
||||
void SetupLineReceivedHandler() {
|
||||
auto handler = [&](boost::system::error_code ec, std::size_t /*bytes_received*/) {
|
||||
if (!ec) {
|
||||
std::istream stream(&streambuf);
|
||||
std::string command;
|
||||
std::getline(stream, command);
|
||||
|
||||
auto reply = console.HandleCommand(command);
|
||||
boost::asio::write(*client, boost::asio::buffer(*reply, reply->size()));
|
||||
}
|
||||
|
||||
SetupLineReceivedHandler();
|
||||
};
|
||||
boost::asio::async_read_until(*client, streambuf, '\n', handler);
|
||||
}
|
||||
|
||||
public:
|
||||
NetworkConsoleServer(Console& console, uint16_t port) : SimpleTCPServer(port), console(console) {}
|
||||
};
|
||||
|
||||
NetworkConsole::NetworkConsole(unsigned port) : server(std::make_unique<NetworkConsoleServer>(*this, port)) {
|
||||
}
|
||||
|
||||
NetworkConsole::~NetworkConsole() = default;
|
||||
|
||||
void NetworkConsole::Run() {
|
||||
server->RunTCPServer();
|
||||
}
|
||||
|
||||
void NetworkConsole::Stop() {
|
||||
server->StopTCPServer();
|
||||
}
|
64
source/framework/console.hpp
Normal file
64
source/framework/console.hpp
Normal file
|
@ -0,0 +1,64 @@
|
|||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* Abstract interface for defining console modules, which are handlers for
|
||||
* a group of commands. For instance, the group of commands starting with "os."
|
||||
* will all be handled by a single ConsoleModule.
|
||||
*/
|
||||
class ConsoleModule {
|
||||
public:
|
||||
virtual ~ConsoleModule() = default;
|
||||
|
||||
/**
|
||||
* Perform the action specified by the given command
|
||||
* @param command Command to run (excludes the module name prefix)
|
||||
* @return User-facing reply string. boost::none is considered failure to handle the command.
|
||||
* @todo Change the parameter to std::string_view type
|
||||
*/
|
||||
virtual std::optional<std::string> HandleCommand(const std::string& command) = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Interface to the console.
|
||||
*/
|
||||
class Console {
|
||||
std::map<std::string /*module_name*/, std::unique_ptr<ConsoleModule>> modules;
|
||||
|
||||
public:
|
||||
virtual ~Console() = default;
|
||||
|
||||
/**
|
||||
* Perform the action specified by the given command
|
||||
* @param command Command to run
|
||||
* @return User-facing reply string. boost::none is considered failure to handle the command.
|
||||
*/
|
||||
std::optional<std::string> HandleCommand(const std::string& command);
|
||||
|
||||
/**
|
||||
* Register the given module using the given category name.
|
||||
*/
|
||||
void RegisterModule(const std::string& name, std::unique_ptr<ConsoleModule> module);
|
||||
};
|
||||
|
||||
/**
|
||||
* Console implementation that waits for incoming connections on a given port
|
||||
* and consecutively reads console commands from any clients.
|
||||
*/
|
||||
class NetworkConsole : public Console {
|
||||
std::unique_ptr<class NetworkConsoleServer> server;
|
||||
|
||||
public:
|
||||
NetworkConsole(unsigned port);
|
||||
~NetworkConsole();
|
||||
|
||||
// Blocks until Stop() is called
|
||||
void Run();
|
||||
|
||||
// Must be called before object destruction
|
||||
void Stop();
|
||||
};
|
43
source/framework/exceptions.cpp
Normal file
43
source/framework/exceptions.cpp
Normal file
|
@ -0,0 +1,43 @@
|
|||
#include "exceptions.hpp"
|
||||
|
||||
#if __has_include(<libunwind.h>)
|
||||
#define UNW_LOCAL_ONLY
|
||||
#include <libunwind.h>
|
||||
#endif
|
||||
|
||||
#include <boost/core/demangle.hpp>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
namespace Mikage::Exceptions {
|
||||
|
||||
std::string generate_backtrace() {
|
||||
std::string ret;
|
||||
|
||||
#if __has_include(<libunwind.h>)
|
||||
unw_cursor_t cursor; unw_context_t uc;
|
||||
unw_word_t ip, sp;
|
||||
|
||||
char name[256];
|
||||
|
||||
unw_getcontext(&uc);
|
||||
unw_init_local(&cursor, &uc);
|
||||
|
||||
int frame = 0;
|
||||
while (unw_step(&cursor) > 0) {
|
||||
unw_get_reg(&cursor, UNW_REG_IP, &ip);
|
||||
unw_get_reg(&cursor, UNW_REG_SP, &sp);
|
||||
unw_word_t offp;
|
||||
unw_get_proc_name(&cursor, name, sizeof(name), &offp);
|
||||
ret += fmt::format("{:>3} {:#014x} in {} (sp={:#014x})\n", "#" + std::to_string(frame++), ip, boost::core::demangle(name).c_str(), sp);
|
||||
}
|
||||
#endif
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::string ContractViolated::FormatMessage(std::string_view condition, std::string_view function, std::string_view file, int line) {
|
||||
return fmt::format( "Failed assertion '{}' in {} ({}:{})\n",
|
||||
condition, function, std::filesystem::path(file).filename().c_str(), line);
|
||||
}
|
||||
|
||||
} // namespace Mikage::Exceptions
|
90
source/framework/exceptions.hpp
Normal file
90
source/framework/exceptions.hpp
Normal file
|
@ -0,0 +1,90 @@
|
|||
#pragma once
|
||||
|
||||
#include <cassert>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <fmt/core.h>
|
||||
|
||||
namespace Mikage {
|
||||
|
||||
namespace Exceptions {
|
||||
|
||||
std::string generate_backtrace();
|
||||
|
||||
/**
|
||||
* Exception to throw upon violation internal contracts that are expected to
|
||||
* hold regardless of invalid emulation or user inputs.
|
||||
*
|
||||
* ValidateContract provides a drop-in replacement for assert that throws
|
||||
* exceptions of this kind.
|
||||
*/
|
||||
struct ContractViolated : std::runtime_error {
|
||||
ContractViolated(std::string_view condition, std::string_view function, std::string_view file, int line)
|
||||
: std::runtime_error(FormatMessage(condition, function, file, line) + generate_backtrace()) {
|
||||
}
|
||||
|
||||
static std::string FormatMessage(std::string_view condition, std::string_view function, std::string_view file, int line);
|
||||
};
|
||||
|
||||
/**
|
||||
* Exception to throw when the given situation is valid but handling it is not
|
||||
* implemented currently.
|
||||
*
|
||||
* Examples of this are unhandled (but valid) enum inputs to emulated APIs or
|
||||
* unhandled corner cases in internal libraries.
|
||||
*
|
||||
* This exception may also be used in situations where it's not entirely clear
|
||||
* whether the behavior may or may not be technically valid.
|
||||
*/
|
||||
struct NotImplemented : std::runtime_error {
|
||||
template<typename... T>
|
||||
NotImplemented(const char* message, T&&... ts) : std::runtime_error(fmt::format(fmt::runtime(message), std::forward<T>(ts)...) + "\n" + generate_backtrace()) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Exception to throw when the given situation is invalid, such as by violating
|
||||
* preconditions of an emulated API.
|
||||
*
|
||||
* Examples of this are out-of-range parameters and invalid enum values.
|
||||
*
|
||||
* When it's not entirely clear whether a situation should be considered
|
||||
* valid or not, prefer NotImplemented.
|
||||
*/
|
||||
struct Invalid : std::runtime_error {
|
||||
template<typename... T>
|
||||
Invalid(const char* message, T&&... ts) : std::runtime_error(fmt::format(fmt::runtime(message), std::forward<T>(ts)...) + "\n" + generate_backtrace()) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Exception to throw when the user provides invalid input, for example from
|
||||
* a command-line argument or a configuration file.
|
||||
*
|
||||
* Emulation logic should never use this.
|
||||
*/
|
||||
struct InvalidUserInput : std::runtime_error {
|
||||
template<typename... T>
|
||||
InvalidUserInput(const char* message, T&&... ts) : std::runtime_error(fmt::format(fmt::runtime(message), std::forward<T>(ts)...) + "\n" + generate_backtrace()) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Exceptions
|
||||
|
||||
} // namespace Mikage
|
||||
|
||||
// ValidateContract is implemented as a macro so that we can stringify the failed condition
|
||||
#ifdef NDEBUG
|
||||
#define ValidateContract(cond) do { if (!(cond)) { \
|
||||
throw Mikage::Exceptions::ContractViolated(#cond, __PRETTY_FUNCTION__, __FILE__, __LINE__); \
|
||||
} } while (false)
|
||||
#else
|
||||
// Use assert() directly in debug mode
|
||||
#define ValidateContract(cond) do { if (!(cond)) { \
|
||||
fputs(Mikage::Exceptions::generate_backtrace().c_str(), stderr); \
|
||||
assert((cond)); \
|
||||
} } while (false)
|
||||
#endif
|
224
source/framework/formats.hpp
Normal file
224
source/framework/formats.hpp
Normal file
|
@ -0,0 +1,224 @@
|
|||
#pragma once
|
||||
|
||||
#include <boost/endian/arithmetic.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <iosfwd>
|
||||
#include <functional>
|
||||
|
||||
namespace FileFormat {
|
||||
|
||||
enum class Endianness {
|
||||
Big,
|
||||
Little,
|
||||
Undefined
|
||||
};
|
||||
|
||||
template<Endianness E>
|
||||
struct endianness_tag {
|
||||
static constexpr auto endianness = E;
|
||||
};
|
||||
|
||||
using big_endian_tag = endianness_tag<Endianness::Big>;
|
||||
using little_endian_tag = endianness_tag<Endianness::Little>;
|
||||
|
||||
struct expected_size_tag_base { };
|
||||
|
||||
/// Used to annotate serializeable structures with the expected total size of the serialized data (used as a static assertion)
|
||||
template<size_t Size>
|
||||
struct expected_size_tag : expected_size_tag_base { static constexpr auto expected_serialized_size = Size; };
|
||||
|
||||
template<typename T>
|
||||
struct StreamIn;
|
||||
|
||||
template<typename T>
|
||||
struct StreamOut;
|
||||
|
||||
/*template<>
|
||||
struct StreamIn<std::istream> {
|
||||
static constexpr bool IsStreamInInstance = true;
|
||||
|
||||
// TODO: Consider changing this to a template for size? Or better yet, the storage type?
|
||||
void Read(char* dest, size_t size);
|
||||
|
||||
private:
|
||||
std::istream stream;
|
||||
};*/
|
||||
|
||||
template<typename ForwardIt, typename EndIt = ForwardIt>
|
||||
struct StreamInFromContainer {
|
||||
StreamInFromContainer(ForwardIt begin, EndIt end) : cursor(begin), end(end) {
|
||||
}
|
||||
|
||||
void Read(char* dest, size_t size) {
|
||||
// static_assert(sizeof(typename std::iterator_traits<ForwardIt>::value_type) == sizeof(char), "");
|
||||
// TODO: Assert we don't copy past the end!
|
||||
for (auto remaining = size; remaining != 0; --remaining) {
|
||||
assert(cursor != end);
|
||||
*dest++ = *cursor++;
|
||||
}
|
||||
}
|
||||
|
||||
operator std::function<void(char*, size_t)>() {
|
||||
return [this](char* dest, size_t size) {
|
||||
Read(dest, size);
|
||||
};
|
||||
}
|
||||
|
||||
static constexpr bool IsStreamInInstance = true;
|
||||
|
||||
private:
|
||||
ForwardIt cursor;
|
||||
EndIt end;
|
||||
};
|
||||
|
||||
template<typename ForwardIt, typename EndIt = ForwardIt>
|
||||
StreamInFromContainer<ForwardIt, EndIt> MakeStreamInFromContainer(ForwardIt begin, EndIt end) {
|
||||
return StreamInFromContainer<ForwardIt, EndIt>{begin, end};
|
||||
}
|
||||
|
||||
template<typename Rng>
|
||||
auto MakeStreamInFromContainer(Rng&& rng) -> StreamInFromContainer<decltype(std::begin(rng)), decltype(std::end(rng))> {
|
||||
return StreamInFromContainer { std::begin(rng), std::end(rng) };
|
||||
}
|
||||
|
||||
template<typename ForwardIt>
|
||||
struct StreamOutFromContainer {
|
||||
StreamOutFromContainer(ForwardIt begin) : cursor(begin) {
|
||||
}
|
||||
|
||||
void Write(char* data, size_t size) {
|
||||
for (auto remaining = size; remaining != 0; --remaining)
|
||||
*cursor++ = *data++;
|
||||
}
|
||||
|
||||
static constexpr bool IsStreamOutInstance = true;
|
||||
|
||||
private:
|
||||
ForwardIt cursor;
|
||||
};
|
||||
|
||||
template<typename ForwardIt>
|
||||
StreamOutFromContainer<ForwardIt> MakeStreamOutFromContainer(ForwardIt begin) {
|
||||
return StreamOutFromContainer<ForwardIt>{begin};
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps given type to a buffer type. The buffer type must be convertible to the type.
|
||||
* For instance, the type uint32_t would map to little_uint32_t.
|
||||
*/
|
||||
template<typename T>
|
||||
struct TypeMapper3DS;
|
||||
|
||||
template<>
|
||||
struct TypeMapper3DS<uint64_t> {
|
||||
using type = boost::endian::little_uint64_at;
|
||||
};
|
||||
|
||||
template<>
|
||||
struct TypeMapper3DS<uint32_t> {
|
||||
using type = boost::endian::little_uint32_at;
|
||||
};
|
||||
|
||||
template<>
|
||||
struct TypeMapper3DS<uint16_t> {
|
||||
using type = boost::endian::little_uint16_at;
|
||||
};
|
||||
|
||||
template<>
|
||||
struct TypeMapper3DS<uint8_t> {
|
||||
using type = boost::endian::little_uint8_at;
|
||||
};
|
||||
|
||||
template<typename T, size_t size>
|
||||
struct TypeMapper3DS<std::array<T, size>> {
|
||||
using type = std::array<typename TypeMapper3DS<T>::type, size>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps given type to a buffer type. The buffer type must be convertible to the type.
|
||||
* For instance, the type uint32_t would map to little_uint32_t.
|
||||
*/
|
||||
template<typename T>
|
||||
struct TypeMapperBigEndian;
|
||||
|
||||
template<>
|
||||
struct TypeMapperBigEndian<uint64_t> {
|
||||
using type = boost::endian::big_uint64_at;
|
||||
};
|
||||
|
||||
template<>
|
||||
struct TypeMapperBigEndian<uint32_t> {
|
||||
using type = boost::endian::big_uint32_at;
|
||||
};
|
||||
|
||||
template<>
|
||||
struct TypeMapperBigEndian<uint16_t> {
|
||||
using type = boost::endian::big_uint16_at;
|
||||
};
|
||||
|
||||
template<>
|
||||
struct TypeMapperBigEndian<uint8_t> {
|
||||
using type = boost::endian::big_uint8_at;
|
||||
};
|
||||
|
||||
template<typename T, size_t size>
|
||||
struct TypeMapperBigEndian<std::array<T, size>> {
|
||||
using type = std::array<typename TypeMapperBigEndian<T>::type, size>;
|
||||
};
|
||||
|
||||
// TODO: TypeMapper for enum -> TypeMapper<underlyingtype>
|
||||
// TODO: TypeMapper for union -> TypeMapper<uintN_t<sizeof(T)>>, iff the union has a tag "TypeMap_to_storage"
|
||||
|
||||
/**
|
||||
* Helper class to offload compilation-time intensive part of the implementation in a standalone
|
||||
* translation unit. The implementation is quite heavy on metaprogramming and hence would make
|
||||
* compile time explode if we were to recompile it in each unit. Unfortunately, this means we
|
||||
* cannot have a proper generic implementation in terms of the Stream parameter, but the overhead
|
||||
* of std::function is probably small compared to the actual file reading operation anyway.
|
||||
* NOTE: This is a template struct rather than a template function because that eases explicit instantiation in the main translation unit.
|
||||
*/
|
||||
template<typename Data>
|
||||
struct SerializationInterface {
|
||||
static Data Load(std::function<void(char*, size_t)> reader);
|
||||
static void Save(const Data& data, std::function<void(char*, size_t)> writer);
|
||||
};
|
||||
|
||||
template<typename Data, typename Stream>
|
||||
Data Load(Stream& stream_in) {
|
||||
static_assert(Stream::IsStreamInInstance, "Given stream must have an IsStreamInInstance member");
|
||||
|
||||
return SerializationInterface<Data>::Load([&stream_in](char* dest, size_t size) {stream_in.Read(dest, size); });
|
||||
}
|
||||
|
||||
template<typename Data>
|
||||
Data Load(std::istream& istream) {
|
||||
// Wrap in a dummy enable_if_t so that this header doesn't need to pull in <iostream> (only users of this function do)
|
||||
auto& istream2 = static_cast<std::enable_if_t<(sizeof(Data) > 0), std::istream&>>(istream);
|
||||
return SerializationInterface<Data>::Load([&istream2](char* dest, size_t size) {istream2.read(dest, size); });
|
||||
}
|
||||
|
||||
template<typename Data, boost::endian::order Order>
|
||||
Data LoadValue(std::function<void(char*, size_t)> reader) {
|
||||
boost::endian::endian_buffer<Order, Data, sizeof(Data) * 8> buffer;
|
||||
reader(reinterpret_cast<char*>(buffer.data()), sizeof(Data));
|
||||
return buffer.value();
|
||||
}
|
||||
|
||||
template<typename Data, typename Stream>
|
||||
auto Save(const Data& data, Stream& stream_out) -> std::enable_if_t<Stream::IsStreamOutInstance> {
|
||||
static_assert(Stream::IsStreamOutInstance, "Given stream must have an IsStreamOutInstance member");
|
||||
|
||||
SerializationInterface<Data>::Save(data, [&stream_out](char* data, size_t size) {stream_out.Write(data, size); });
|
||||
}
|
||||
|
||||
// Wrap in a dummy enable_if_t so that this header doesn't need to pull in <iostream> (only users of this function do)
|
||||
template<typename Data>
|
||||
void Save(const Data& data, std::ostream& ostream) {
|
||||
// Wrap in a dummy enable_if_t so that this header doesn't need to pull in <iostream> (only users of this function do)
|
||||
auto& ostream2 = static_cast<std::enable_if_t<(sizeof(Data) > 0), std::ostream&>>(ostream);
|
||||
SerializationInterface<Data>::Save(data, [&ostream2](char* data, size_t size) {ostream2.write(data, size); });
|
||||
}
|
||||
|
||||
} // namespace FileFormat
|
362
source/framework/formats_impl.hpp
Normal file
362
source/framework/formats_impl.hpp
Normal file
|
@ -0,0 +1,362 @@
|
|||
/**
|
||||
* The purpose of this file is to provide the definition of LoadWithReadCallback.
|
||||
* This function is heavy on template metaprogramming, so we don't want to
|
||||
* recompile it in every translation unit. Instead, each serialized structure
|
||||
* should have exactly one file associated to it that includes this header and
|
||||
* then explicitly instantiates LoadWithReadCallback.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifndef FORMATS_IMPL_EXPLICIT_FORMAT_INSTANTIATIONS_INTENDED
|
||||
#error This file should only be included if you want to explicitly instantiate LoadWithReadCallback for user-defined structures
|
||||
#endif
|
||||
|
||||
#include <framework/formats.hpp>
|
||||
#include <framework/meta_tools.hpp>
|
||||
|
||||
#include <boost/hana/tuple.hpp>
|
||||
#include <boost/hana/ext/std/tuple.hpp>
|
||||
#include <boost/hana/transform.hpp>
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace FileFormat {
|
||||
|
||||
/// Type-erasing helper class to allow compiling the deserialization code only once
|
||||
template<typename T, template<typename> class TypeMapper>
|
||||
struct StreamReader {
|
||||
T operator()(std::function<void(char*, size_t)>& reader) const {
|
||||
typename TypeMapper<T>::type val;
|
||||
reader(reinterpret_cast<char*>(&val), sizeof(val));
|
||||
return T { val };
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T, size_t N, template<typename> class TypeMapper>
|
||||
struct StreamReader<std::array<T, N>, TypeMapper> {
|
||||
auto operator()(std::function<void(char*, size_t)>& reader) const {
|
||||
std::array<T, N> ret;
|
||||
for (auto& val : ret)
|
||||
val = StreamReader<T, TypeMapper>(reader);
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T, template<typename> class TypeMapper>
|
||||
struct StreamWriter {
|
||||
void operator()(const T& val, std::function<void(char*, size_t)>& writer) const {
|
||||
// TODO: Check if Data is brace-intializable from T...
|
||||
|
||||
typename TypeMapper<T>::type mapped_val{val};
|
||||
writer(reinterpret_cast<char*>(&mapped_val), sizeof(mapped_val));
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T, size_t N, template<typename> class TypeMapper>
|
||||
struct StreamWriter<std::array<T, N>, TypeMapper> {
|
||||
void operator()(const std::array<T, N>& val, std::function<void(char*, size_t)>& writer) const {
|
||||
for (auto& elem : val)
|
||||
StreamWriter<T, TypeMapper>{}(elem, writer);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
using void_t = void;
|
||||
|
||||
namespace detail {
|
||||
|
||||
struct DefaultTags : endianness_tag<Endianness::Undefined>, expected_size_tag<0> {
|
||||
};
|
||||
|
||||
template<typename Tags, typename NewData, typename = void>
|
||||
struct SelectEndiannessTagT {
|
||||
using type = endianness_tag<Tags::endianness>;
|
||||
};
|
||||
template<typename Tags, typename NewData>
|
||||
struct SelectEndiannessTagT<Tags, NewData, Meta::void_t<decltype(NewData::Tags::endianness)>> {
|
||||
using type = endianness_tag<NewData::Tags::endianness>;
|
||||
};
|
||||
template<typename Tags, typename NewData>
|
||||
using SelectEndiannessTag = typename SelectEndiannessTagT<Tags, NewData>::type;
|
||||
|
||||
template<typename Tags, typename NewData, typename = void>
|
||||
struct SelectExpectedSizeTagT {
|
||||
// No size expectation for the new data unless explicitly provided
|
||||
using type = expected_size_tag<0>;
|
||||
};
|
||||
template<typename Tags, typename NewData>
|
||||
struct SelectExpectedSizeTagT<Tags, NewData, Meta::void_t<decltype(NewData::Tags::expected_serialized_size)>> {
|
||||
using type = expected_size_tag<NewData::Tags::expected_serialized_size>;
|
||||
};
|
||||
template<typename Tags, typename NewData>
|
||||
using SelectExpectedSizeTag = typename SelectExpectedSizeTagT<Tags, NewData>::type;
|
||||
|
||||
} // namespace detail
|
||||
|
||||
template<typename DataTags, typename NewData>
|
||||
using MergeTags = Meta::inherited<detail::SelectEndiannessTag<DataTags, NewData>,
|
||||
detail::SelectExpectedSizeTag<DataTags, NewData>>;
|
||||
|
||||
template<typename Data>
|
||||
using TagsOf = MergeTags<detail::DefaultTags, Data>;
|
||||
|
||||
template<typename Tags, typename TestTag>
|
||||
static constexpr auto HasTag = std::is_base_of<TestTag, Tags>::value;
|
||||
|
||||
template<typename DataTags>
|
||||
struct HasBigEndianTag : std::integral_constant<bool, HasTag<DataTags, big_endian_tag>> {
|
||||
|
||||
};
|
||||
|
||||
template<bool Cond, template<typename T> class OnTrue, template<typename T> class OnFalse, typename T>
|
||||
using MetaConditional = typename std::conditional<Cond, OnTrue<T>, OnFalse<T>>::type;
|
||||
|
||||
template<typename DataTags>
|
||||
struct Mapper {
|
||||
template<typename T>
|
||||
using mapper_template = MetaConditional<HasBigEndianTag<DataTags>::value, TypeMapperBigEndian, TypeMapper3DS, T>;
|
||||
};
|
||||
|
||||
namespace hana = boost::hana;
|
||||
|
||||
template<typename T>
|
||||
struct is_std_array : std::false_type {};
|
||||
|
||||
template<typename T, size_t N>
|
||||
struct is_std_array<std::array<T, N>> : std::true_type {};
|
||||
|
||||
template<typename T, typename = void>
|
||||
struct DataToTupleT;
|
||||
|
||||
template<typename T>
|
||||
struct DataToTupleT<T, typename std::enable_if<std::is_class<T>::value && !is_std_array<T>::value>::type> {
|
||||
auto operator()(const T& data) const {
|
||||
return hana::to<hana::ext::std::tuple_tag>(hana::transform(hana::to_tuple(data), hana::second));
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
struct DataToTupleT<T, typename std::enable_if<!std::is_class<T>::value || is_std_array<T>::value>::type> {
|
||||
auto operator()(const T& data) const {
|
||||
return std::tuple<T>(data);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T, typename = void>
|
||||
struct MembersAsTuple;
|
||||
|
||||
template<typename T>
|
||||
struct MembersAsTuple<T, typename std::enable_if<std::is_class<T>::value && !is_std_array<T>::value>::type> {
|
||||
using type = decltype(DataToTupleT<T>{}(std::declval<T>()));
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
struct MembersAsTuple<T, typename std::enable_if<!std::is_class<T>::value || is_std_array<T>::value>::type> {
|
||||
using type = std::tuple<T>;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
using MembersAsTuple_t = typename MembersAsTuple<T>::type;
|
||||
|
||||
template<typename T, typename = void_t<T>>
|
||||
struct SerializedSize : std::integral_constant<size_t, sizeof(T)> { };
|
||||
|
||||
|
||||
template<size_t... Nums>
|
||||
struct MakeSum;
|
||||
|
||||
template<>
|
||||
struct MakeSum<> : std::integral_constant<size_t, 0> {};
|
||||
|
||||
// TODO: Static assert against overflows!
|
||||
template<size_t Num1, size_t... Nums>
|
||||
struct MakeSum<Num1, Nums...> : std::integral_constant<size_t, Num1 + MakeSum<Nums...>::value> {};
|
||||
|
||||
template<size_t... Nums>
|
||||
struct MakeProduct;
|
||||
|
||||
template<>
|
||||
struct MakeProduct<> : std::integral_constant<size_t, 1> {};
|
||||
|
||||
// TODO: Static assert against overflows!
|
||||
template<size_t Num1, size_t... Nums>
|
||||
struct MakeProduct<Num1, Nums...> : std::integral_constant<size_t, Num1 * MakeProduct<Nums...>::value> {};
|
||||
|
||||
|
||||
// Non-array structure types only
|
||||
template<typename T>
|
||||
struct SerializedSize<T, typename std::enable_if<Meta::is_class_v<T> && !Meta::is_std_array_v<T> && !Meta::is_std_tuple_v<T>>::type>
|
||||
: SerializedSize<MembersAsTuple_t<T>> { };
|
||||
|
||||
// Arrays only
|
||||
template<typename T, size_t N>
|
||||
struct SerializedSize<std::array<T, N>, void>
|
||||
: MakeProduct<N, SerializedSize<T>::value> { };
|
||||
|
||||
// Non-structure and non-tuple types
|
||||
template<typename T>
|
||||
struct SerializedSize<T, typename std::enable_if<!Meta::is_class_v<T> && !Meta::is_std_tuple_v<T>>::type>
|
||||
: std::integral_constant<size_t, sizeof(T)> { };
|
||||
|
||||
// Collections of types
|
||||
template<typename... T>
|
||||
struct SerializedSize<std::tuple<T...>, void> : MakeSum<SerializedSize<T>::value...> {};
|
||||
|
||||
|
||||
template<typename DataTags, size_t ActualSize, typename = void>
|
||||
struct CheckSizeExpectations {
|
||||
// No size expectations specified, so just return a match
|
||||
};
|
||||
|
||||
template<typename DataTags, size_t ActualSize>
|
||||
struct CheckSizeExpectations<DataTags, ActualSize, typename std::enable_if<DataTags::expected_serialized_size != 0>::type> {
|
||||
static_assert(Meta::CHECK_EQUALITY_VERBOSELY<ActualSize, DataTags::expected_serialized_size>::value, "");
|
||||
};
|
||||
|
||||
template<typename, typename, typename, typename = void>
|
||||
struct ConstructFromReadCallback;
|
||||
|
||||
// Non-array structure types only
|
||||
template<typename Data, typename DataTags, typename... T>
|
||||
struct ConstructFromReadCallback<Data, DataTags, std::tuple<T...>, typename std::enable_if<std::is_class<Data>::value && !is_std_array<Data>::value>::type> {
|
||||
// TODO: Check if Data is brace-intializable from T...
|
||||
|
||||
Data operator()(std::function<void(char*, size_t)>& reader) const {
|
||||
(void)CheckSizeExpectations<DataTags, SerializedSize<Data>::value>{};
|
||||
|
||||
namespace hana = boost::hana;
|
||||
return Data { ConstructFromReadCallback<T, MergeTags<DataTags, T>, MembersAsTuple_t<T>>{}(reader)... };
|
||||
}
|
||||
};
|
||||
|
||||
// Arrays only
|
||||
template<typename Data, typename DataTags, typename... T>
|
||||
struct ConstructFromReadCallback<Data, DataTags, std::tuple<T...>, typename std::enable_if<is_std_array<Data>::value>::type> {
|
||||
// TODO: Check if Data is brace-intializable from T...
|
||||
|
||||
Data operator()(std::function<void(char*, size_t)>& reader) const {
|
||||
using Value = typename Data::value_type;
|
||||
|
||||
Data ret;
|
||||
for (auto& val : ret)
|
||||
val = ConstructFromReadCallback<Value, MergeTags<DataTags, Value>, std::tuple<Value>>{}(reader);
|
||||
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
// Non-array plain elements only
|
||||
template<typename Data, typename DataTags, typename... T>
|
||||
struct ConstructFromReadCallback<Data, DataTags, std::tuple<T...>, typename std::enable_if<!std::is_class<Data>::value && !is_std_array<Data>::value>::type> {
|
||||
// TODO: Check if Data is brace-intializable from T...
|
||||
|
||||
Data operator()(std::function<void(char*, size_t)>& reader) const {
|
||||
// TODO: Why do we bother using T here at all? Should be sufficient to just use Data directly...
|
||||
|
||||
return Data { StreamReader<T, Mapper<DataTags>::template mapper_template>{}(reader)... };
|
||||
}
|
||||
};
|
||||
|
||||
template<typename Data>
|
||||
Data SerializationInterface<Data>::Load(std::function<void(char*, size_t)> reader) {
|
||||
// For small data, read to a stack buffer first
|
||||
std::array<char, sizeof(Data)> raw;
|
||||
if (sizeof(Data) <= 0x200) {
|
||||
size_t read_size = sizeof(Data);
|
||||
if constexpr (TagsOf<Data>::expected_serialized_size != 0) {
|
||||
read_size = Data::Tags::expected_serialized_size;
|
||||
}
|
||||
reader(raw.data(), read_size);
|
||||
reader = [&raw, offset = size_t { 0 }](char* dest, size_t size) mutable {
|
||||
memcpy(dest, raw.data() + offset, size);
|
||||
offset += size;
|
||||
};
|
||||
}
|
||||
|
||||
namespace hana = boost::hana;
|
||||
using hana_tuple = decltype(hana::to_tuple(std::declval<Data>()));
|
||||
using hana_types_tuple = decltype(hana::transform(hana_tuple{}, hana::second));
|
||||
using TupleType = decltype(hana::to<hana::ext::std::tuple_tag>(hana_types_tuple{}));
|
||||
return ConstructFromReadCallback<Data, TagsOf<Data>, TupleType>{}(reader);
|
||||
}
|
||||
|
||||
template<>
|
||||
uint32_t SerializationInterface<uint32_t>::Load(std::function<void(char*, size_t)> reader);
|
||||
|
||||
template<>
|
||||
boost::endian::little_uint32_t SerializationInterface<boost::endian::little_uint32_t>::Load(std::function<void(char*, size_t)> reader);
|
||||
|
||||
template<>
|
||||
boost::endian::big_uint32_t SerializationInterface<boost::endian::big_uint32_t>::Load(std::function<void(char*, size_t)> reader);
|
||||
|
||||
|
||||
template<typename, typename, typename, typename = void>
|
||||
struct SaveWithWriteCallback;
|
||||
|
||||
template<size_t index, typename DataTags, typename MembersTuple, typename = void>
|
||||
struct SaveWithWriteCallbackHelper {
|
||||
void operator()(const MembersTuple& data, std::function<void(char*, size_t)>& writer) {
|
||||
// Do nothing if index is larger than the tuple size
|
||||
}
|
||||
};
|
||||
|
||||
template<size_t index, typename DataTags, typename MembersTuple>
|
||||
//struct SaveWithWriteCallbackHelper<index, DataTags, MembersTuple, void_t<decltype(std::get<index>(std::declval<MembersTuple>()))>> {
|
||||
struct SaveWithWriteCallbackHelper<index, DataTags, MembersTuple, std::enable_if_t<(index < Meta::tuple_size_v<MembersTuple>)>> {
|
||||
void operator()(const MembersTuple& data, std::function<void(char*, size_t)>& writer) {
|
||||
// using SubType = std::remove_cv_t<std::remove_reference_t<decltype(std::get<index>(data))>>;
|
||||
using SubType = std::remove_cv_t<std::remove_reference_t<std::tuple_element_t<index, MembersTuple>>>;
|
||||
SaveWithWriteCallback<SubType, MergeTags<DataTags, SubType>, MembersAsTuple_t<SubType>>{}(std::get<index>(data), writer);
|
||||
SaveWithWriteCallbackHelper<index + 1, DataTags, MembersTuple>{}(data, writer);
|
||||
}
|
||||
};
|
||||
|
||||
// Non-array structure types only
|
||||
template<typename Data, typename DataTags, typename... T>
|
||||
struct SaveWithWriteCallback<Data, DataTags, std::tuple<T...>, typename std::enable_if<std::is_class<Data>::value && !is_std_array<Data>::value>::type> {
|
||||
void operator()(const Data& data, std::function<void(char*, size_t)>& writer) const {
|
||||
(void)CheckSizeExpectations<DataTags, SerializedSize<Data>::value>{};
|
||||
|
||||
namespace hana = boost::hana;
|
||||
SaveWithWriteCallbackHelper<0, DataTags, MembersAsTuple_t<Data>>{}(DataToTupleT<Data>{}(data), writer);
|
||||
}
|
||||
};
|
||||
|
||||
// Arrays only
|
||||
template<typename Data, typename DataTags, typename... T>
|
||||
struct SaveWithWriteCallback<Data, DataTags, std::tuple<T...>, typename std::enable_if<is_std_array<Data>::value>::type> {
|
||||
void operator()(const Data& data, std::function<void(char*, size_t)>& writer) const {
|
||||
using Value = typename Data::value_type;
|
||||
|
||||
for (auto& val : data)
|
||||
SaveWithWriteCallback<Value, MergeTags<DataTags, Value>, std::tuple<Value>>{}(val, writer);
|
||||
}
|
||||
};
|
||||
|
||||
// Non-array plain elements only
|
||||
template<typename Data, typename DataTags, typename... T>
|
||||
struct SaveWithWriteCallback<Data, DataTags, std::tuple<T...>, typename std::enable_if<!std::is_class<Data>::value && !is_std_array<Data>::value>::type> {
|
||||
void operator()(const Data& data, std::function<void(char*, size_t)>& writer) const {
|
||||
StreamWriter<Data, Mapper<DataTags>::template mapper_template>{}(data, writer);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename Data>
|
||||
void SerializationInterface<Data>::Save(const Data& data, std::function<void(char*, size_t)> writer) {
|
||||
namespace hana = boost::hana;
|
||||
using hana_tuple = decltype(hana::to_tuple(std::declval<Data>()));
|
||||
using hana_types_tuple = decltype(hana::transform(hana_tuple{}, hana::second));
|
||||
using TupleType = decltype(hana::to<hana::ext::std::tuple_tag>(hana_types_tuple{}));
|
||||
SaveWithWriteCallback<Data, TagsOf<Data>, TupleType>{}(data, writer);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SerializationInterface<uint32_t>::Save(const uint32_t& data, std::function<void(char*, size_t)> writer) {
|
||||
SaveWithWriteCallback<uint32_t, detail::DefaultTags, std::tuple<uint32_t>>{}(data, writer);
|
||||
}
|
||||
|
||||
template<>
|
||||
void SerializationInterface<boost::endian::big_uint32_t>::Save(const boost::endian::big_uint32_t& data, std::function<void(char*, size_t)> writer);
|
||||
|
||||
} // namespace FileFormat
|
172
source/framework/image_format.hpp
Normal file
172
source/framework/image_format.hpp
Normal file
|
@ -0,0 +1,172 @@
|
|||
#pragma once
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <stdexcept>
|
||||
|
||||
enum class GenericImageFormat {
|
||||
// Color framebuffer formats
|
||||
RGBA8,
|
||||
RGB8,
|
||||
RGB565,
|
||||
RGBA5551,
|
||||
RGBA4,
|
||||
|
||||
// Depth-stencil buffer formats
|
||||
D16,
|
||||
D24,
|
||||
D24S8,
|
||||
|
||||
// Texture formats
|
||||
IA8,
|
||||
RG8,
|
||||
I8,
|
||||
A8,
|
||||
IA4,
|
||||
I4,
|
||||
A4,
|
||||
ETC1, // compressed
|
||||
ETC1A4, // compressed
|
||||
|
||||
Unknown
|
||||
};
|
||||
|
||||
template<> struct fmt::formatter<GenericImageFormat> : fmt::formatter<std::string_view> {
|
||||
template<typename FormatContext>
|
||||
auto format(GenericImageFormat format, FormatContext& ctx) -> decltype(ctx.out()) {
|
||||
std::string_view name = std::invoke([format]() -> std::string_view {
|
||||
switch (format) {
|
||||
case GenericImageFormat::RGBA8: return "RGBA8";
|
||||
case GenericImageFormat::RGB8: return "RGB8";
|
||||
case GenericImageFormat::RGB565: return "RGB565";
|
||||
case GenericImageFormat::RGBA5551: return "RGBA5551";
|
||||
case GenericImageFormat::RGBA4: return "RGBA4";
|
||||
case GenericImageFormat::D16: return "D16";
|
||||
case GenericImageFormat::D24: return "D24";
|
||||
case GenericImageFormat::D24S8: return "D24S8";
|
||||
case GenericImageFormat::IA8: return "IA8";
|
||||
case GenericImageFormat::RG8: return "RG8";
|
||||
case GenericImageFormat::I8: return "I8";
|
||||
case GenericImageFormat::A8: return "A8";
|
||||
case GenericImageFormat::IA4: return "IA4";
|
||||
case GenericImageFormat::I4: return "I4";
|
||||
case GenericImageFormat::A4: return "A4";
|
||||
case GenericImageFormat::ETC1: return "ETC1";
|
||||
case GenericImageFormat::ETC1A4: return "ETC1A4";
|
||||
|
||||
default:
|
||||
throw std::runtime_error("formatter: Unknown GenericImageFormat");
|
||||
}
|
||||
});
|
||||
return formatter<std::string_view>::format(name, ctx);
|
||||
}
|
||||
};
|
||||
|
||||
static constexpr uint32_t NibblesPerPixel(GenericImageFormat format) {
|
||||
switch (format) {
|
||||
case GenericImageFormat::RGBA8:
|
||||
case GenericImageFormat::D24S8:
|
||||
return 8;
|
||||
|
||||
case GenericImageFormat::RGB8:
|
||||
case GenericImageFormat::D24:
|
||||
return 6;
|
||||
|
||||
case GenericImageFormat::RGB565:
|
||||
case GenericImageFormat::RGBA5551:
|
||||
case GenericImageFormat::RGBA4:
|
||||
case GenericImageFormat::D16:
|
||||
case GenericImageFormat::IA8:
|
||||
case GenericImageFormat::RG8:
|
||||
return 4;
|
||||
|
||||
case GenericImageFormat::I8:
|
||||
case GenericImageFormat::A8:
|
||||
case GenericImageFormat::IA4:
|
||||
return 2;
|
||||
|
||||
case GenericImageFormat::I4:
|
||||
case GenericImageFormat::A4:
|
||||
return 1;
|
||||
|
||||
default:
|
||||
throw std::runtime_error("NibblesPerPixel: Unknown GenericImageFormat");
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr uint32_t TextureSize(GenericImageFormat format, uint32_t width, uint32_t height) {
|
||||
switch (format) {
|
||||
case GenericImageFormat::ETC1:
|
||||
case GenericImageFormat::ETC1A4:
|
||||
{
|
||||
// TODO: Untested
|
||||
if ((width % 8) || (height % 8)) {
|
||||
throw std::runtime_error("Unaligned ETC texture dimensions");
|
||||
}
|
||||
// Previous had the wrong size here. citrace-player research uncovered these textures should be larger
|
||||
auto bpp = (format == GenericImageFormat::ETC1) ? 1 : 2;
|
||||
// return (width / 8) * 4 * bpp * (height / 8);
|
||||
return width * bpp * height / 2;
|
||||
}
|
||||
|
||||
default:
|
||||
return width * height * NibblesPerPixel(format) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
template<typename Type>
|
||||
constexpr GenericImageFormat ToGenericFormat(Type format) {
|
||||
auto [raw] = format;
|
||||
if (raw < std::size(Type::format_map)) {
|
||||
return Type::format_map[raw];
|
||||
} else {
|
||||
return GenericImageFormat::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
template<typename Type>
|
||||
constexpr Type FromGenericFormat(GenericImageFormat format) {
|
||||
for (uint32_t raw_target_format = 0; raw_target_format < std::size(Type::format_map); ++raw_target_format) {
|
||||
if (Type::format_map[raw_target_format] == format) {
|
||||
return Type { raw_target_format };
|
||||
}
|
||||
}
|
||||
|
||||
throw std::runtime_error("Unknown GenericImageFormat");
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
constexpr bool operator ==(T format, GenericImageFormat reference) {
|
||||
auto [raw_left] = format;
|
||||
auto [raw_right] = FromGenericFormat<T>(reference);
|
||||
return (raw_left == raw_right);
|
||||
}
|
||||
|
||||
template<typename Enum>
|
||||
[[maybe_unused]] constexpr static uint32_t NibblesPerPixel(Enum format) {
|
||||
return NibblesPerPixel(ToGenericFormat(format));
|
||||
}
|
||||
|
||||
template<typename To, typename From>
|
||||
[[maybe_unused]] constexpr static To ConvertFormatTo(From format) {
|
||||
return FromGenericFormat<To>(ToGenericFormat(format));
|
||||
}
|
||||
|
||||
template<typename Type>
|
||||
constexpr auto GetStorage(Type format) {
|
||||
// The format may only have one member, which is the storage
|
||||
auto [storage] = format;
|
||||
static_assert(sizeof(storage) == sizeof(format), "Format type may not have more than one member");
|
||||
return storage;
|
||||
}
|
||||
|
||||
template<typename Type>
|
||||
Type FromRawValue(decltype(GetStorage(std::declval<Type>())) value) {
|
||||
if (value >= std::size(Type::format_map)) {
|
||||
throw std::runtime_error("Failed to construct format from raw value");
|
||||
}
|
||||
return Type { value };
|
||||
}
|
29
source/framework/logging.hpp
Normal file
29
source/framework/logging.hpp
Normal file
|
@ -0,0 +1,29 @@
|
|||
#pragma once
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
class LogManager {
|
||||
spdlog::sink_ptr sink;
|
||||
std::unordered_map<std::string, std::shared_ptr<spdlog::logger>> loggers;
|
||||
|
||||
public:
|
||||
LogManager(spdlog::sink_ptr sink_) : sink(sink_) {
|
||||
}
|
||||
|
||||
void ChangeSink(spdlog::sink_ptr new_sink) {
|
||||
sink = new_sink;
|
||||
}
|
||||
|
||||
std::shared_ptr<spdlog::logger> GetLogger(const std::string& name) {
|
||||
return loggers.at(name);
|
||||
}
|
||||
|
||||
std::shared_ptr<spdlog::logger> RegisterLogger(std::string name) {
|
||||
auto logger = std::make_shared<spdlog::logger>(name, sink);
|
||||
logger->set_pattern("[%n] [%l] %v");
|
||||
auto ret = loggers.emplace(std::move(name), std::move(logger));
|
||||
return ret.first->second;
|
||||
}
|
||||
};
|
617
source/framework/math_vec.hpp
Normal file
617
source/framework/math_vec.hpp
Normal file
|
@ -0,0 +1,617 @@
|
|||
// Copyright 2014 Tony Wasserka
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above copyright
|
||||
// notice, this list of conditions and the following disclaimer in the
|
||||
// documentation and/or other materials provided with the distribution.
|
||||
// * Neither the name of the owner nor the names of its contributors may
|
||||
// be used to endorse or promote products derived from this software
|
||||
// without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace Math {
|
||||
|
||||
template<typename T> class Vec2;
|
||||
template<typename T> class Vec3;
|
||||
template<typename T> class Vec4;
|
||||
|
||||
template<typename T>
|
||||
static inline Vec2<T> MakeVec(const T& x, const T& y);
|
||||
template<typename T>
|
||||
static inline Vec3<T> MakeVec(const T& x, const T& y, const T& z);
|
||||
template<typename T>
|
||||
static inline Vec4<T> MakeVec(const T& x, const T& y, const T& z, const T& w);
|
||||
|
||||
|
||||
template<typename T>
|
||||
class Vec2 {
|
||||
public:
|
||||
T x;
|
||||
T y;
|
||||
|
||||
T* AsArray() { return &x; }
|
||||
|
||||
Vec2() = default;
|
||||
Vec2(const T a[2]) : x(a[0]), y(a[1]) {}
|
||||
Vec2(const T& _x, const T& _y) : x(_x), y(_y) {}
|
||||
|
||||
template<typename T2>
|
||||
Vec2<T2> Cast() const {
|
||||
return Vec2<T2>((T2)x, (T2)y);
|
||||
}
|
||||
|
||||
static Vec2 AssignToAll(const T& f)
|
||||
{
|
||||
return Vec2<T>(f, f);
|
||||
}
|
||||
|
||||
void Write(T a[2])
|
||||
{
|
||||
a[0] = x; a[1] = y;
|
||||
}
|
||||
|
||||
Vec2<decltype(T{}+T{})> operator +(const Vec2& other) const
|
||||
{
|
||||
return MakeVec(x+other.x, y+other.y);
|
||||
}
|
||||
void operator += (const Vec2 &other)
|
||||
{
|
||||
x+=other.x; y+=other.y;
|
||||
}
|
||||
Vec2<decltype(T{}-T{})> operator -(const Vec2& other) const
|
||||
{
|
||||
return MakeVec(x-other.x, y-other.y);
|
||||
}
|
||||
void operator -= (const Vec2& other)
|
||||
{
|
||||
x-=other.x; y-=other.y;
|
||||
}
|
||||
Vec2<decltype(-T{})> operator -() const
|
||||
{
|
||||
return MakeVec(-x,-y);
|
||||
}
|
||||
Vec2<decltype(T{}*T{})> operator * (const Vec2& other) const
|
||||
{
|
||||
return MakeVec(x*other.x, y*other.y);
|
||||
}
|
||||
template<typename V>
|
||||
Vec2<decltype(T{}*V{})> operator * (const V& f) const
|
||||
{
|
||||
return MakeVec(x*f,y*f);
|
||||
}
|
||||
template<typename V>
|
||||
void operator *= (const V& f)
|
||||
{
|
||||
x*=f; y*=f;
|
||||
}
|
||||
template<typename V>
|
||||
Vec2<decltype(T{}/V{})> operator / (const V& f) const
|
||||
{
|
||||
return MakeVec(x/f,y/f);
|
||||
}
|
||||
template<typename V>
|
||||
void operator /= (const V& f)
|
||||
{
|
||||
*this = *this / f;
|
||||
}
|
||||
|
||||
T Length2() const
|
||||
{
|
||||
return x*x + y*y;
|
||||
}
|
||||
|
||||
// Only implemented for T=float
|
||||
float Length() const;
|
||||
void SetLength(const float l);
|
||||
Vec2 WithLength(const float l) const;
|
||||
float Distance2To(Vec2 &other);
|
||||
Vec2 Normalized() const;
|
||||
float Normalize(); // returns the previous length, which is often useful
|
||||
|
||||
T& operator [] (int i) //allow vector[1] = 3 (vector.y=3)
|
||||
{
|
||||
return *((&x) + i);
|
||||
}
|
||||
T operator [] (const int i) const
|
||||
{
|
||||
return *((&x) + i);
|
||||
}
|
||||
|
||||
void SetZero()
|
||||
{
|
||||
x=0; y=0;
|
||||
}
|
||||
|
||||
// Common aliases: UV (texel coordinates), ST (texture coordinates)
|
||||
T& u() { return x; }
|
||||
T& v() { return y; }
|
||||
T& s() { return x; }
|
||||
T& t() { return y; }
|
||||
|
||||
const T& u() const { return x; }
|
||||
const T& v() const { return y; }
|
||||
const T& s() const { return x; }
|
||||
const T& t() const { return y; }
|
||||
|
||||
// swizzlers - create a subvector of specific components
|
||||
const Vec2 yx() const { return Vec2(y, x); }
|
||||
const Vec2 vu() const { return Vec2(y, x); }
|
||||
const Vec2 ts() const { return Vec2(y, x); }
|
||||
};
|
||||
|
||||
template<typename T, typename V>
|
||||
Vec2<T> operator * (const V& f, const Vec2<T>& vec)
|
||||
{
|
||||
return Vec2<T>(f*vec.x,f*vec.y);
|
||||
}
|
||||
|
||||
typedef Vec2<float> Vec2f;
|
||||
|
||||
template<typename T>
|
||||
class Vec3
|
||||
{
|
||||
public:
|
||||
T x;
|
||||
T y;
|
||||
T z;
|
||||
|
||||
T* AsArray() { return &x; }
|
||||
|
||||
Vec3() = default;
|
||||
Vec3(const T a[3]) : x(a[0]), y(a[1]), z(a[2]) {}
|
||||
Vec3(const T& _x, const T& _y, const T& _z) : x(_x), y(_y), z(_z) {}
|
||||
|
||||
template<typename T2>
|
||||
Vec3<T2> Cast() const {
|
||||
return MakeVec<T2>((T2)x, (T2)y, (T2)z);
|
||||
}
|
||||
|
||||
// Only implemented for T=int and T=float
|
||||
static Vec3 FromRGB(unsigned int rgb);
|
||||
unsigned int ToRGB() const; // alpha bits set to zero
|
||||
|
||||
static Vec3 AssignToAll(const T& f)
|
||||
{
|
||||
return MakeVec(f, f, f);
|
||||
}
|
||||
|
||||
void Write(T a[3])
|
||||
{
|
||||
a[0] = x; a[1] = y; a[2] = z;
|
||||
}
|
||||
|
||||
Vec3<decltype(T{}+T{})> operator +(const Vec3 &other) const
|
||||
{
|
||||
return MakeVec(x+other.x, y+other.y, z+other.z);
|
||||
}
|
||||
void operator += (const Vec3 &other)
|
||||
{
|
||||
x+=other.x; y+=other.y; z+=other.z;
|
||||
}
|
||||
Vec3<decltype(T{}-T{})> operator -(const Vec3 &other) const
|
||||
{
|
||||
return MakeVec(x-other.x, y-other.y, z-other.z);
|
||||
}
|
||||
void operator -= (const Vec3 &other)
|
||||
{
|
||||
x-=other.x; y-=other.y; z-=other.z;
|
||||
}
|
||||
Vec3<decltype(-T{})> operator -() const
|
||||
{
|
||||
return MakeVec(-x,-y,-z);
|
||||
}
|
||||
Vec3<decltype(T{}*T{})> operator * (const Vec3 &other) const
|
||||
{
|
||||
return MakeVec(x*other.x, y*other.y, z*other.z);
|
||||
}
|
||||
template<typename V>
|
||||
Vec3<decltype(T{}*V{})> operator * (const V& f) const
|
||||
{
|
||||
return MakeVec(x*f,y*f,z*f);
|
||||
}
|
||||
template<typename V>
|
||||
void operator *= (const V& f)
|
||||
{
|
||||
x*=f; y*=f; z*=f;
|
||||
}
|
||||
template<typename V>
|
||||
Vec3<decltype(T{}/V{})> operator / (const V& f) const
|
||||
{
|
||||
return MakeVec(x/f,y/f,z/f);
|
||||
}
|
||||
template<typename V>
|
||||
void operator /= (const V& f)
|
||||
{
|
||||
*this = *this / f;
|
||||
}
|
||||
|
||||
T Length2() const
|
||||
{
|
||||
return x*x + y*y + z*z;
|
||||
}
|
||||
|
||||
// Only implemented for T=float
|
||||
float Length() const;
|
||||
void SetLength(const float l);
|
||||
Vec3 WithLength(const float l) const;
|
||||
float Distance2To(Vec3 &other);
|
||||
Vec3 Normalized() const;
|
||||
float Normalize(); // returns the previous length, which is often useful
|
||||
|
||||
T& operator [] (int i) //allow vector[2] = 3 (vector.z=3)
|
||||
{
|
||||
return *((&x) + i);
|
||||
}
|
||||
T operator [] (const int i) const
|
||||
{
|
||||
return *((&x) + i);
|
||||
}
|
||||
|
||||
void SetZero()
|
||||
{
|
||||
x=0; y=0; z=0;
|
||||
}
|
||||
|
||||
// Common aliases: UVW (texel coordinates), RGB (colors), STQ (texture coordinates)
|
||||
T& u() { return x; }
|
||||
T& v() { return y; }
|
||||
T& w() { return z; }
|
||||
|
||||
T& r() { return x; }
|
||||
T& g() { return y; }
|
||||
T& b() { return z; }
|
||||
|
||||
T& s() { return x; }
|
||||
T& t() { return y; }
|
||||
T& q() { return z; }
|
||||
|
||||
const T& u() const { return x; }
|
||||
const T& v() const { return y; }
|
||||
const T& w() const { return z; }
|
||||
|
||||
const T& r() const { return x; }
|
||||
const T& g() const { return y; }
|
||||
const T& b() const { return z; }
|
||||
|
||||
const T& s() const { return x; }
|
||||
const T& t() const { return y; }
|
||||
const T& q() const { return z; }
|
||||
|
||||
// swizzlers - create a subvector of specific components
|
||||
// e.g. Vec2 uv() { return Vec2(x,y); }
|
||||
// _DEFINE_SWIZZLER2 defines a single such function, DEFINE_SWIZZLER2 defines all of them for all component names (x<->r) and permutations (xy<->yx)
|
||||
#define _DEFINE_SWIZZLER2(a, b, name) const Vec2<T> name() const { return Vec2<T>(a, b); }
|
||||
#define DEFINE_SWIZZLER2(a, b, a2, b2, a3, b3, a4, b4) \
|
||||
_DEFINE_SWIZZLER2(a, b, a##b); \
|
||||
_DEFINE_SWIZZLER2(a, b, a2##b2); \
|
||||
_DEFINE_SWIZZLER2(a, b, a3##b3); \
|
||||
_DEFINE_SWIZZLER2(a, b, a4##b4); \
|
||||
_DEFINE_SWIZZLER2(b, a, b##a); \
|
||||
_DEFINE_SWIZZLER2(b, a, b2##a2); \
|
||||
_DEFINE_SWIZZLER2(b, a, b3##a3); \
|
||||
_DEFINE_SWIZZLER2(b, a, b4##a4)
|
||||
|
||||
DEFINE_SWIZZLER2(x, y, r, g, u, v, s, t);
|
||||
DEFINE_SWIZZLER2(x, z, r, b, u, w, s, q);
|
||||
DEFINE_SWIZZLER2(y, z, g, b, v, w, t, q);
|
||||
#undef DEFINE_SWIZZLER2
|
||||
#undef _DEFINE_SWIZZLER2
|
||||
};
|
||||
|
||||
template<typename T, typename V>
|
||||
Vec3<T> operator * (const V& f, const Vec3<T>& vec)
|
||||
{
|
||||
return Vec3<T>(f*vec.x,f*vec.y,f*vec.z);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline float Vec3<float>::Length() const {
|
||||
return std::sqrt(x * x + y * y + z * z);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline Vec3<float> Vec3<float>::Normalized() const {
|
||||
return *this / Length();
|
||||
}
|
||||
|
||||
|
||||
typedef Vec3<float> Vec3f;
|
||||
|
||||
template<typename T>
|
||||
class Vec4
|
||||
{
|
||||
public:
|
||||
T x;
|
||||
T y;
|
||||
T z;
|
||||
T w;
|
||||
|
||||
T* AsArray() { return &x; }
|
||||
|
||||
Vec4() = default;
|
||||
Vec4(const T a[4]) : x(a[0]), y(a[1]), z(a[2]), w(a[3]) {}
|
||||
Vec4(const T& _x, const T& _y, const T& _z, const T& _w) : x(_x), y(_y), z(_z), w(_w) {}
|
||||
|
||||
template<typename T2>
|
||||
Vec4<T2> Cast() const {
|
||||
return Vec4<T2>((T2)x, (T2)y, (T2)z, (T2)w);
|
||||
}
|
||||
|
||||
// Only implemented for T=int and T=float
|
||||
static Vec4 FromRGBA(unsigned int rgba);
|
||||
unsigned int ToRGBA() const;
|
||||
|
||||
static Vec4 AssignToAll(const T& f) {
|
||||
return Vec4<T>(f, f, f, f);
|
||||
}
|
||||
|
||||
void Write(T a[4])
|
||||
{
|
||||
a[0] = x; a[1] = y; a[2] = z; a[3] = w;
|
||||
}
|
||||
|
||||
Vec4<decltype(T{}+T{})> operator +(const Vec4& other) const
|
||||
{
|
||||
return MakeVec(x+other.x, y+other.y, z+other.z, w+other.w);
|
||||
}
|
||||
void operator += (const Vec4& other)
|
||||
{
|
||||
x+=other.x; y+=other.y; z+=other.z; w+=other.w;
|
||||
}
|
||||
Vec4<decltype(T{}-T{})> operator -(const Vec4 &other) const
|
||||
{
|
||||
return MakeVec(x-other.x, y-other.y, z-other.z, w-other.w);
|
||||
}
|
||||
void operator -= (const Vec4 &other)
|
||||
{
|
||||
x-=other.x; y-=other.y; z-=other.z; w-=other.w;
|
||||
}
|
||||
Vec4<decltype(-T{})> operator -() const
|
||||
{
|
||||
return MakeVec(-x,-y,-z,-w);
|
||||
}
|
||||
Vec4<decltype(T{}*T{})> operator * (const Vec4 &other) const
|
||||
{
|
||||
return MakeVec(x*other.x, y*other.y, z*other.z, w*other.w);
|
||||
}
|
||||
template<typename V>
|
||||
Vec4<decltype(T{}*V{})> operator * (const V& f) const
|
||||
{
|
||||
return MakeVec(x*f,y*f,z*f,w*f);
|
||||
}
|
||||
template<typename V>
|
||||
void operator *= (const V& f)
|
||||
{
|
||||
x*=f; y*=f; z*=f; w*=f;
|
||||
}
|
||||
template<typename V>
|
||||
Vec4<decltype(T{}/V{})> operator / (const V& f) const
|
||||
{
|
||||
return MakeVec(x/f,y/f,z/f,w/f);
|
||||
}
|
||||
template<typename V>
|
||||
void operator /= (const V& f)
|
||||
{
|
||||
*this = *this / f;
|
||||
}
|
||||
|
||||
T Length2() const
|
||||
{
|
||||
return x*x + y*y + z*z + w*w;
|
||||
}
|
||||
|
||||
// Only implemented for T=float
|
||||
float Length() const;
|
||||
void SetLength(const float l);
|
||||
Vec4 WithLength(const float l) const;
|
||||
float Distance2To(Vec4 &other);
|
||||
Vec4 Normalized() const;
|
||||
float Normalize(); // returns the previous length, which is often useful
|
||||
|
||||
T& operator [] (int i) //allow vector[2] = 3 (vector.z=3)
|
||||
{
|
||||
return *((&x) + i);
|
||||
}
|
||||
T operator [] (const int i) const
|
||||
{
|
||||
return *((&x) + i);
|
||||
}
|
||||
|
||||
void SetZero()
|
||||
{
|
||||
x=0; y=0; z=0;
|
||||
}
|
||||
|
||||
// Common alias: RGBA (colors)
|
||||
T& r() { return x; }
|
||||
T& g() { return y; }
|
||||
T& b() { return z; }
|
||||
T& a() { return w; }
|
||||
|
||||
const T& r() const { return x; }
|
||||
const T& g() const { return y; }
|
||||
const T& b() const { return z; }
|
||||
const T& a() const { return w; }
|
||||
|
||||
// swizzlers - create a subvector of specific components
|
||||
// e.g. Vec2 uv() { return Vec2(x,y); }
|
||||
// _DEFINE_SWIZZLER2 defines a single such function, DEFINE_SWIZZLER2 defines all of them for all component names (x<->r) and permutations (xy<->yx)
|
||||
#define _DEFINE_SWIZZLER2(a, b, name) const Vec2<T> name() const { return Vec2<T>(a, b); }
|
||||
#define DEFINE_SWIZZLER2(a, b, a2, b2) \
|
||||
_DEFINE_SWIZZLER2(a, b, a##b); \
|
||||
_DEFINE_SWIZZLER2(a, b, a2##b2); \
|
||||
_DEFINE_SWIZZLER2(b, a, b##a); \
|
||||
_DEFINE_SWIZZLER2(b, a, b2##a2)
|
||||
|
||||
DEFINE_SWIZZLER2(x, y, r, g);
|
||||
DEFINE_SWIZZLER2(x, z, r, b);
|
||||
DEFINE_SWIZZLER2(x, w, r, a);
|
||||
DEFINE_SWIZZLER2(y, z, g, b);
|
||||
DEFINE_SWIZZLER2(y, w, g, a);
|
||||
DEFINE_SWIZZLER2(z, w, b, a);
|
||||
#undef DEFINE_SWIZZLER2
|
||||
#undef _DEFINE_SWIZZLER2
|
||||
|
||||
#define _DEFINE_SWIZZLER3(a, b, c, name) const Vec3<T> name() const { return Vec3<T>(a, b, c); }
|
||||
#define DEFINE_SWIZZLER3(a, b, c, a2, b2, c2) \
|
||||
_DEFINE_SWIZZLER3(a, b, c, a##b##c); \
|
||||
_DEFINE_SWIZZLER3(a, c, b, a##c##b); \
|
||||
_DEFINE_SWIZZLER3(b, a, c, b##a##c); \
|
||||
_DEFINE_SWIZZLER3(b, c, a, b##c##a); \
|
||||
_DEFINE_SWIZZLER3(c, a, b, c##a##b); \
|
||||
_DEFINE_SWIZZLER3(c, b, a, c##b##a); \
|
||||
_DEFINE_SWIZZLER3(a, b, c, a2##b2##c2); \
|
||||
_DEFINE_SWIZZLER3(a, c, b, a2##c2##b2); \
|
||||
_DEFINE_SWIZZLER3(b, a, c, b2##a2##c2); \
|
||||
_DEFINE_SWIZZLER3(b, c, a, b2##c2##a2); \
|
||||
_DEFINE_SWIZZLER3(c, a, b, c2##a2##b2); \
|
||||
_DEFINE_SWIZZLER3(c, b, a, c2##b2##a2)
|
||||
|
||||
DEFINE_SWIZZLER3(x, y, z, r, g, b);
|
||||
DEFINE_SWIZZLER3(x, y, w, r, g, a);
|
||||
DEFINE_SWIZZLER3(x, z, w, r, b, a);
|
||||
DEFINE_SWIZZLER3(y, z, w, g, b, a);
|
||||
#undef DEFINE_SWIZZLER3
|
||||
#undef _DEFINE_SWIZZLER3
|
||||
};
|
||||
|
||||
|
||||
template<typename T, typename V>
|
||||
Vec4<decltype(V{}*T{})> operator * (const V& f, const Vec4<T>& vec)
|
||||
{
|
||||
return MakeVec(f*vec.x,f*vec.y,f*vec.z,f*vec.w);
|
||||
}
|
||||
|
||||
typedef Vec4<float> Vec4f;
|
||||
|
||||
|
||||
template<typename T>
|
||||
static inline decltype(T{}*T{}+T{}*T{}) Dot(const Vec2<T>& a, const Vec2<T>& b)
|
||||
{
|
||||
return a.x*b.x + a.y*b.y;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static inline decltype(T{}*T{}+T{}*T{}) Dot(const Vec3<T>& a, const Vec3<T>& b)
|
||||
{
|
||||
return a.x*b.x + a.y*b.y + a.z*b.z;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static inline decltype(T{}*T{}+T{}*T{}) Dot(const Vec4<T>& a, const Vec4<T>& b)
|
||||
{
|
||||
return a.x*b.x + a.y*b.y + a.z*b.z + a.w*b.w;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static inline Vec3<decltype(T{}*T{}-T{}*T{})> Cross(const Vec3<T>& a, const Vec3<T>& b)
|
||||
{
|
||||
return MakeVec(a.y*b.z-a.z*b.y, a.z*b.x-a.x*b.z, a.x*b.y-a.y*b.x);
|
||||
}
|
||||
|
||||
// linear interpolation via float: 0.0=begin, 1.0=end
|
||||
template<typename X>
|
||||
static inline decltype(X{}*float{}+X{}*float{}) Lerp(const X& begin, const X& end, const float t)
|
||||
{
|
||||
return begin*(1.f-t) + end*t;
|
||||
}
|
||||
|
||||
// linear interpolation via int: 0=begin, base=end
|
||||
template<typename X, int base>
|
||||
static inline decltype((X{}*int{}+X{}*int{}) / base) LerpInt(const X& begin, const X& end, const int t)
|
||||
{
|
||||
return (begin*(base-t) + end*t) / base;
|
||||
}
|
||||
|
||||
// Utility vector factories
|
||||
template<typename T>
|
||||
static inline Vec2<T> MakeVec(const T& x, const T& y)
|
||||
{
|
||||
return Vec2<T>{x, y};
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static inline Vec3<T> MakeVec(const T& x, const T& y, const T& z)
|
||||
{
|
||||
return Vec3<T>{x, y, z};
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static inline Vec4<T> MakeVec(const T& x, const T& y, const Vec2<T>& zw)
|
||||
{
|
||||
return MakeVec(x, y, zw[0], zw[1]);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static inline Vec3<T> MakeVec(const Vec2<T>& xy, const T& z)
|
||||
{
|
||||
return MakeVec(xy[0], xy[1], z);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static inline Vec3<T> MakeVec(const T& x, const Vec2<T>& yz)
|
||||
{
|
||||
return MakeVec(x, yz[0], yz[1]);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static inline Vec4<T> MakeVec(const T& x, const T& y, const T& z, const T& w)
|
||||
{
|
||||
return Vec4<T>{x, y, z, w};
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static inline Vec4<T> MakeVec(const Vec2<T>& xy, const T& z, const T& w)
|
||||
{
|
||||
return MakeVec(xy[0], xy[1], z, w);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static inline Vec4<T> MakeVec(const T& x, const Vec2<T>& yz, const T& w)
|
||||
{
|
||||
return MakeVec(x, yz[0], yz[1], w);
|
||||
}
|
||||
|
||||
// NOTE: This has priority over "Vec2<Vec2<T>> MakeVec(const Vec2<T>& x, const Vec2<T>& y)".
|
||||
// Even if someone wanted to use an odd object like Vec2<Vec2<T>>, the compiler would error
|
||||
// out soon enough due to misuse of the returned structure.
|
||||
template<typename T>
|
||||
static inline Vec4<T> MakeVec(const Vec2<T>& xy, const Vec2<T>& zw)
|
||||
{
|
||||
return MakeVec(xy[0], xy[1], zw[0], zw[1]);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static inline Vec4<T> MakeVec(const Vec3<T>& xyz, const T& w)
|
||||
{
|
||||
return MakeVec(xyz[0], xyz[1], xyz[2], w);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static inline Vec4<T> MakeVec(const T& x, const Vec3<T>& yzw)
|
||||
{
|
||||
return MakeVec(x, yzw[0], yzw[1], yzw[2]);
|
||||
}
|
||||
|
||||
|
||||
} // namespace
|
215
source/framework/meta_tools.hpp
Normal file
215
source/framework/meta_tools.hpp
Normal file
|
@ -0,0 +1,215 @@
|
|||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <type_traits>
|
||||
|
||||
namespace Meta {
|
||||
|
||||
template<typename... T>
|
||||
using void_t = void;
|
||||
|
||||
// Evaluate to false unless this template is specialized for T
|
||||
// TODO: Once we move to C++17, make the template take a generic "auto..." parameter pack
|
||||
template<typename T, template<typename...> class Template>
|
||||
struct is_instantiation_of : std::false_type {};
|
||||
|
||||
// If a set of template arguments that produce T exists, evaluate to true
|
||||
template<typename... Args, template<typename...> class Template>
|
||||
struct is_instantiation_of<Template<Args...>, Template> : std::true_type {};
|
||||
|
||||
// Specialized variant of the above to deal with the std::array template argument list
|
||||
template<typename T, template<typename, std::size_t> class Template>
|
||||
struct is_instantiation_of2 : std::false_type {};
|
||||
template<typename Arg1, std::size_t Arg2, template<typename, std::size_t> class Template>
|
||||
struct is_instantiation_of2<Template<Arg1, Arg2>, Template> : std::true_type {};
|
||||
|
||||
template<typename T>
|
||||
constexpr auto is_std_tuple_v = is_instantiation_of<T, std::tuple>::value;
|
||||
|
||||
template<typename T>
|
||||
constexpr auto is_std_array_v = is_instantiation_of2<T, std::array>::value;
|
||||
|
||||
/// Produce a structure that inherits all given Base classes
|
||||
template<typename... Bases>
|
||||
struct inherited : Bases... {
|
||||
};
|
||||
|
||||
/**
|
||||
* Turn the given template template argument in a type, such that it can be
|
||||
* passed to standard type functors like std::enable_if without having to
|
||||
* evaluate the template with any particular arguments
|
||||
*/
|
||||
template<template<typename...> class Temp>
|
||||
struct delay {
|
||||
template<typename... Ts>
|
||||
using eval = Temp<Ts...>;
|
||||
};
|
||||
|
||||
/// Conditionally selects one of the two given templates depending on the given boolean
|
||||
template<bool Cond, template<typename T> class OnTrue, template<typename T> class OnFalse>
|
||||
struct MetaConditional {
|
||||
template<typename T>
|
||||
using eval = typename std::conditional_t<Cond, delay<OnTrue>, delay<OnFalse>>::type::template eval<T>;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
constexpr auto is_class_v = std::is_class<T>::value;
|
||||
template<typename B, typename C>
|
||||
constexpr auto is_base_of_v = std::is_base_of<B, C>::value;
|
||||
template<typename T>
|
||||
constexpr auto tuple_size_v = std::tuple_size<T>::value;
|
||||
template<typename T, typename U>
|
||||
constexpr auto is_same_v = std::is_same<T, U>::value;
|
||||
|
||||
namespace detail {
|
||||
template<bool NoError>
|
||||
struct CHECK_EQUALITY_VERBOSELY_DEFAULT_ERROR { static_assert(NoError, "Given integers are not equal!"); };
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper structure for asserting that the compile-time constants A and B
|
||||
* are equal. If they aren't, the type Error<true> will be instantiated, which
|
||||
* is supposed to abort compilation due to a static_assert failure.
|
||||
*
|
||||
* The error message can be customized by passing an own Error template, which
|
||||
* is expected to have a static_assert fail if its instantiated with a "true"
|
||||
* argument. This is useful to give helpful error messages instead of cryptic
|
||||
* "5 != 3" messages.
|
||||
*/
|
||||
template<int A, int B,
|
||||
template<bool Err> class Error = detail::CHECK_EQUALITY_VERBOSELY_DEFAULT_ERROR>
|
||||
struct CHECK_EQUALITY_VERBOSELY
|
||||
{
|
||||
// Force Error<A==B> to be instantiated and error out if A!=B
|
||||
// If A==B, use sizeof to form the value "true".
|
||||
static constexpr bool value = sizeof(Error<A==B>);
|
||||
};
|
||||
|
||||
|
||||
template<typename F, typename... Args>
|
||||
decltype(auto) invoke(F&& f, Args&&... args) noexcept(noexcept(std::forward<F>(f)(std::forward<Args>(args)...))) {
|
||||
return std::forward<F>(f)(std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
//template<typename T>
|
||||
//using typeof_foo = decltype(std::declval<T>().foo);
|
||||
|
||||
/*template<template<typename...> class Templ, typename ArgsTuple, typename = void_t<ArgsTuple>>
|
||||
struct is_valid_impl : std::false_type {};
|
||||
|
||||
template<template<typename...> class Templ, typename... Args>
|
||||
struct is_valid_impl<Templ, std::tuple<Args...>, Templ<Args...>> : std::true_type {};
|
||||
|
||||
template<template<typename...> class Templ, typename... Args>
|
||||
using is_valid = is_valid_impl<Templ, std::tuple<Args...>>;
|
||||
*/
|
||||
|
||||
template<typename AlwaysVoid, template<typename...> class Templ, typename... Args>
|
||||
struct is_valid_impl : std::false_type {};
|
||||
|
||||
template<template<typename...> class Templ, typename... Args>
|
||||
struct is_valid_impl<void_t<Templ<Args...>>, Templ, Args...> : std::true_type {};
|
||||
|
||||
template<template<typename...> class Templ, typename... Args>
|
||||
using is_valid = is_valid_impl<void, Templ, Args...>;
|
||||
|
||||
|
||||
/*template<typename F, typename = void_t<F>>
|
||||
struct is_valid_call_impl : std::false_type {};
|
||||
|
||||
template<typename F, typename... Args>
|
||||
struct is_valid_call_impl<F, void_t<decltype(std::declval<F&&>()(std::declval<Args&&>()...))>> : std::true_type {};
|
||||
|
||||
template<typename F, typename... Args>
|
||||
static constexpr auto is_valid_call(F&& f, Args&&... args) {
|
||||
return is_valid_call_impl<F, Args...>::value;
|
||||
}*/
|
||||
|
||||
/*template<typename, typename F, typename... Args>
|
||||
struct invoke_result;
|
||||
|
||||
template<typename F, typename... Args>
|
||||
struct invoke_result<decltype(std::declval<F>()(std::declval<Args>()...)), F, Args...> {
|
||||
using type = decltype(std::declval<F>()(std::declval<Args>()...));
|
||||
}*/
|
||||
|
||||
template<typename F, typename... Args>
|
||||
using invoke_result = std::invoke_result<F(Args...)>;
|
||||
|
||||
//template<typename F, typename... Args>
|
||||
//using invoke_result_t = invoke_result<F, Args...>::type;
|
||||
|
||||
template<typename F, typename... Args>
|
||||
using invoke_result_t = typename invoke_result<F, Args...>::type;
|
||||
|
||||
|
||||
/**
|
||||
* Helper class for calling a function such that its arguments are evaluated in
|
||||
* sequential order. This couldn't easily be achieved otherwise, since the
|
||||
* C++ standard doesn't guarantee evaluation order for function arguments.
|
||||
* It does guarantee left-to-right evaluation in brace-initialization (cf.
|
||||
* C++ standard draft n4296 ([dcl.init.list] (8.5.4.4))), though, which is why
|
||||
* this class exists.
|
||||
*
|
||||
* Usage example:
|
||||
* char f(int a, int b);
|
||||
* int num = 0;
|
||||
* CallWithSequentialEvaluation { f, ++num, ++num };
|
||||
*
|
||||
* Note that the return type cannot easily be deduced and hence must be
|
||||
* specified manually.
|
||||
*
|
||||
* TODO: Chances are this behavior isn't implemented in all major
|
||||
* compilers. The following may be affected:
|
||||
* * MSVC 2015 Update 3 (untested)
|
||||
* * GCC 7.1.1 (seems to be working for nontrivial cases)
|
||||
* Symptoms of this issue are that IPC command arguments may be
|
||||
* loaded in the wrong order.
|
||||
*/
|
||||
template<typename Result>
|
||||
class CallWithSequentialEvaluation {
|
||||
Result result;
|
||||
|
||||
public:
|
||||
template<typename F, typename... Args>
|
||||
CallWithSequentialEvaluation(F&& f, Args&&... args) : result(std::forward<F>(f)(std::forward<Args>(args)...)) {}
|
||||
|
||||
Result GetResult() && {
|
||||
return std::move(result);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename F, typename... Args>
|
||||
CallWithSequentialEvaluation(F&&, Args&&...) -> CallWithSequentialEvaluation<std::invoke_result_t<F, Args...>>;
|
||||
|
||||
// Specialization for void
|
||||
template<>
|
||||
class CallWithSequentialEvaluation<void> {
|
||||
public:
|
||||
template<typename F, typename... Args>
|
||||
CallWithSequentialEvaluation(F&& f, Args&&... args) {
|
||||
std::forward<F>(f)(std::forward<Args>(args)...);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename F>
|
||||
struct function_traits;
|
||||
|
||||
template<typename Ret, typename... Args>
|
||||
struct function_traits<Ret(Args...)> { using args = std::tuple<Args...>; };
|
||||
|
||||
template<typename Ret, typename... Args>
|
||||
struct function_traits<Ret(*)(Args...)> { using args = std::tuple<Args...>; };
|
||||
|
||||
template<typename Enum>
|
||||
constexpr std::underlying_type_t<Enum> to_underlying(Enum value) noexcept {
|
||||
return static_cast<std::underlying_type_t<Enum>>(value);
|
||||
}
|
||||
|
||||
template<typename Enum>
|
||||
constexpr Enum next_enum(Enum value, std::underlying_type_t<Enum> n = 1) noexcept {
|
||||
return Enum { Meta::to_underlying(value) + n };
|
||||
}
|
||||
|
||||
} // namespace Meta
|
141
source/framework/profiler.cpp
Normal file
141
source/framework/profiler.cpp
Normal file
|
@ -0,0 +1,141 @@
|
|||
#include "profiler.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace Profiler {
|
||||
|
||||
constexpr auto skip_profiler = true;
|
||||
|
||||
Duration DurationMeasure::TimePassedUntil(TimePoint now) const {
|
||||
return std::chrono::duration_cast<Duration>(now - begin);
|
||||
}
|
||||
|
||||
Metric Accumulator::SnapshotFor(TimePoint now) const {
|
||||
// Careful here for race-conditions!
|
||||
|
||||
// If paused, immediately return the metric. If paused gets toggled before returning, metric will be mildy out of date at worst.
|
||||
if (paused) {
|
||||
return metric;
|
||||
}
|
||||
|
||||
// Get the current metric.
|
||||
// TODO: Race condition here - measure.begin might get updated after taking the metric snapshot :/
|
||||
auto snapshot_metric = metric; // TODO: Copying this should be ensured atomic!
|
||||
auto snapshot_measure = measure;
|
||||
|
||||
auto time_passed = snapshot_measure.TimePassedUntil(now);
|
||||
|
||||
// If measurement begin is later than "now" (e.g. due to cross-thread
|
||||
// access), assume the activity was interrupted before and hence no
|
||||
// metrics were gathered.
|
||||
snapshot_metric.total += std::max(time_passed, std::chrono::duration_values<Duration>::zero());
|
||||
|
||||
return snapshot_metric;
|
||||
}
|
||||
|
||||
Activity& Activity::GetSubActivity(std::string_view name) {
|
||||
auto it = std::find_if(sub_activities.begin(), sub_activities.end(), [=](auto& activity) { return activity.name == name; });
|
||||
if (it != sub_activities.end()) {
|
||||
return *it;
|
||||
}
|
||||
|
||||
return sub_activities.emplace_back(*this, name);
|
||||
}
|
||||
|
||||
void Activity::ResumeFrom(TimePoint time) {
|
||||
if constexpr (skip_profiler) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!parent) {
|
||||
// The root activity always stays active
|
||||
return;
|
||||
}
|
||||
|
||||
assert(!IsMeasuring());
|
||||
|
||||
// Resume parent first, just to make sure they always get a slightly higher total metric
|
||||
if (!parent->IsMeasuring()) {
|
||||
parent->ResumeFrom(time);
|
||||
}
|
||||
|
||||
if (!accumulator) {
|
||||
accumulator = std::make_unique<Accumulator>(metric, time);
|
||||
assert(IsMeasuring());
|
||||
} else {
|
||||
assert(!IsMeasuring());
|
||||
accumulator->Resume(time);
|
||||
}
|
||||
}
|
||||
|
||||
void Activity::InterruptAt(TimePoint time) {
|
||||
if constexpr (skip_profiler) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Interrupt children first, to make sure the parent always has a slightly higher total metric
|
||||
for (auto& activity : sub_activities) {
|
||||
if (activity.IsMeasuring()) {
|
||||
activity.InterruptAt(time);
|
||||
}
|
||||
}
|
||||
|
||||
if (!IsMeasuring()) {
|
||||
throw std::runtime_error("Called Interrupt on Activity " + name + " which wasn't active");
|
||||
}
|
||||
accumulator->Interrupt(time);
|
||||
}
|
||||
|
||||
void Activity::Resume() {
|
||||
if constexpr (skip_profiler) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto time = std::chrono::steady_clock::now();
|
||||
ResumeFrom(time);
|
||||
}
|
||||
|
||||
void Activity::Interrupt() {
|
||||
if constexpr (skip_profiler) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto time = std::chrono::steady_clock::now();
|
||||
InterruptAt(time);
|
||||
}
|
||||
|
||||
void Activity::SwitchSubActivity(std::string_view from, std::string_view to) {
|
||||
if constexpr (skip_profiler) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto time = std::chrono::steady_clock::now();
|
||||
GetSubActivity(from).InterruptAt(time);
|
||||
GetSubActivity(to).ResumeFrom(time);
|
||||
}
|
||||
|
||||
FrozenMetrics Activity::GetMetricsAt(TimePoint now) const {
|
||||
// TODO: Race condition here. What if IsMeasuring changes before we take the snapshot?
|
||||
auto metrics = IsMeasuring() ? accumulator->SnapshotFor(now) : metric;
|
||||
|
||||
FrozenMetrics ret { *this, metrics, {} };
|
||||
// TODO: Verify iteration over std::list is thread-safe
|
||||
if constexpr (!skip_profiler) {
|
||||
for (auto& activity : sub_activities) {
|
||||
ret.sub_metrics.emplace_back(activity.GetMetricsAt(now));
|
||||
}
|
||||
}
|
||||
return std::move(ret);
|
||||
}
|
||||
|
||||
FrozenMetrics Profiler::Freeze() const {
|
||||
// TODO: Achieve this without interrupting any activity, and don't assume no activity is running right now!
|
||||
// root_activity->Interrupt();
|
||||
// root_activity->accumulator->Resume();
|
||||
|
||||
return root_activity->GetMetricsAt(std::chrono::steady_clock::now());
|
||||
}
|
||||
|
||||
// TODO: MeasureScope should get the previously active activity and restore it!
|
||||
|
||||
} // namespace Profiler
|
311
source/framework/profiler.hpp
Normal file
311
source/framework/profiler.hpp
Normal file
|
@ -0,0 +1,311 @@
|
|||
#pragma once
|
||||
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <exception>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/mp11/algorithm.hpp>
|
||||
#include <boost/mp11/list.hpp>
|
||||
|
||||
namespace Profiler {
|
||||
|
||||
using TimePoint = std::chrono::time_point<std::chrono::steady_clock>;
|
||||
using Duration = std::chrono::microseconds;
|
||||
|
||||
/**
|
||||
* A DurationMeasure measures the time from a fixed starting point in time to
|
||||
* later moments in time.
|
||||
*/
|
||||
class DurationMeasure {
|
||||
TimePoint begin;
|
||||
|
||||
public:
|
||||
DurationMeasure(TimePoint begin) : begin(begin) {
|
||||
}
|
||||
|
||||
Duration TimePassedUntil(TimePoint) const;
|
||||
};
|
||||
|
||||
struct Metric {
|
||||
/// Total time spend on the given task
|
||||
/// TODO: Should use an atomic for this!
|
||||
Duration total;
|
||||
};
|
||||
|
||||
class Accumulator {
|
||||
friend class Activity;
|
||||
|
||||
Accumulator(const Accumulator&) = delete;
|
||||
Accumulator& operator=(const Accumulator&) = delete;
|
||||
|
||||
Metric& metric;
|
||||
|
||||
DurationMeasure measure;
|
||||
|
||||
bool paused = false;
|
||||
|
||||
public:
|
||||
Accumulator(Metric& metric, TimePoint begin) : metric(metric), measure(begin) {
|
||||
}
|
||||
|
||||
~Accumulator() {
|
||||
}
|
||||
|
||||
void Interrupt(TimePoint end) {
|
||||
metric.total += measure.TimePassedUntil(end);
|
||||
paused = true;
|
||||
}
|
||||
|
||||
/// Resume after prior interruption
|
||||
void Resume(TimePoint begin) {
|
||||
measure = DurationMeasure(begin);
|
||||
paused = false;
|
||||
}
|
||||
|
||||
bool IsPaused() const {
|
||||
return paused;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns metrics for the given TimePoint.
|
||||
* @note If the given time point is earlier than begin, it is assumed that
|
||||
* measuring has been interrupted throughout the entire time between
|
||||
* the two TimePoints.
|
||||
* Thread-safe.
|
||||
*/
|
||||
Metric SnapshotFor(TimePoint) const;
|
||||
};
|
||||
|
||||
class Activity;
|
||||
|
||||
struct FrozenMetrics {
|
||||
const Activity& activity;
|
||||
|
||||
Metric metric;
|
||||
|
||||
std::vector<FrozenMetrics> sub_metrics;
|
||||
};
|
||||
|
||||
/**
|
||||
* The life time of these should last until the end of the program so that references are guaranteed to be stable.
|
||||
*/
|
||||
class Activity {
|
||||
public: // TODO: Iteration interface!
|
||||
std::string name;
|
||||
Metric metric = {};
|
||||
|
||||
// Parent activity, or nullptr for the root activity
|
||||
Activity* parent = nullptr;
|
||||
|
||||
// Using std::list here because we need stable references when appending new activities.
|
||||
// TODO: Use something that has more efficient iteration, and random-access for compile-time-specified activities!
|
||||
// We also need thread-safe iteration while adding new elements at the end of the list.
|
||||
// Invariant: Not more than one of these IsMeasuring at any point in time.
|
||||
// Invariant: !IsMeasuring implies no sub activity IsMeasuring.
|
||||
// NOTE: The first entries are occupied by compile-time activities
|
||||
// std::list<Activity> sub_activities;
|
||||
std::list<Activity> sub_activities;
|
||||
|
||||
// @pre: Only active if parent->accumulator is active
|
||||
// TODO: Why is this a unique_ptr again?
|
||||
std::unique_ptr<Accumulator> accumulator;
|
||||
|
||||
/// Creates the root activity in the measuring state
|
||||
Activity() : name("root") {
|
||||
accumulator = std::make_unique<Accumulator>(metric, std::chrono::steady_clock::now());
|
||||
}
|
||||
|
||||
friend class Profiler;
|
||||
|
||||
bool IsMeasuring() const {
|
||||
return accumulator && !accumulator->IsPaused();
|
||||
}
|
||||
|
||||
void InterruptAt(TimePoint);
|
||||
|
||||
void ResumeFrom(TimePoint);
|
||||
public:
|
||||
/// Creates the activity in the stopped state
|
||||
Activity(Activity& parent, std::string_view name) : name(name), parent(&parent) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up or create new sub task with the given name
|
||||
*/
|
||||
Activity& GetSubActivity(std::string_view name);
|
||||
|
||||
/**
|
||||
* Interrupt all measurements in this activity, including any subactivities
|
||||
*/
|
||||
void Interrupt();
|
||||
|
||||
/**
|
||||
* Starts measurement after construction or resumes measurements on this activity after a prior call to Interrupt().
|
||||
* Resumption propagates to all parent activities up to the root.
|
||||
*/
|
||||
void Resume();
|
||||
|
||||
void SwitchSubActivity(std::string_view from, std::string_view to);
|
||||
|
||||
// Get a snapshot of the metrics for this and all child activities
|
||||
// Thread-safe.
|
||||
FrozenMetrics GetMetricsAt(TimePoint) const;
|
||||
|
||||
std::string_view GetName() const {
|
||||
return name;
|
||||
}
|
||||
};
|
||||
|
||||
/// Compile-time tag used for zero-cost lookup of activities with tags in the Activities namespace
|
||||
template<typename ActivityTag>
|
||||
struct TaggedActivity {
|
||||
using SubTags = typename ActivityTag::tags;
|
||||
|
||||
Activity activity;
|
||||
|
||||
TaggedActivity() {
|
||||
auto populate_activity = [](Activity& activity, auto tag, auto&& self) {
|
||||
boost::mp11::mp_for_each<typename decltype(tag)::tags>([&activity, self](auto subtag) {
|
||||
// activity.sub_activities.reserve(100);
|
||||
auto& sub_activity = activity.GetSubActivity(subtag.name);
|
||||
self(sub_activity, subtag, self);
|
||||
});
|
||||
//// // Trigger creation by activity lookup
|
||||
//// static_cast<void>(activity.GetSubActivity(tag.name));
|
||||
};
|
||||
populate_activity(activity, ActivityTag{}, populate_activity);
|
||||
|
||||
// boost::mp11::mp_for_each<SubTags>([activity=&this->activity](auto tag) {
|
||||
//// // Trigger creation by activity lookup
|
||||
//// static_cast<void>(activity.GetSubActivity(tag.name));
|
||||
// TaggedActivity<decltype(tag)> sub_activity;
|
||||
// activity.sub_activities.emplace_back(std::move(sub_activity.activity));
|
||||
// });
|
||||
}
|
||||
|
||||
Activity& operator*() {
|
||||
return activity;
|
||||
}
|
||||
|
||||
Activity* operator->() {
|
||||
return &activity;
|
||||
}
|
||||
|
||||
const Activity& operator*() const {
|
||||
return activity;
|
||||
}
|
||||
|
||||
const Activity* operator->() const {
|
||||
return &activity;
|
||||
}
|
||||
|
||||
template<typename SubActivityTag>
|
||||
TaggedActivity<SubActivityTag>& GetSubActivity() {
|
||||
constexpr size_t index = boost::mp11::mp_find<SubTags, SubActivityTag>::value;
|
||||
static_assert(index != boost::mp11::mp_size<SubTags>::value, "Given tag is not a child of this activity");
|
||||
|
||||
// reinterpret_cast here is (hopefully) fine since Activity is the only member in TaggedActivity
|
||||
auto& result = *std::next(activity.sub_activities.begin(), index);
|
||||
if (result.name != SubActivityTag::name) {
|
||||
throw std::runtime_error("WTF? " + result.name + " <-> " + std::string{SubActivityTag::name});
|
||||
}
|
||||
return reinterpret_cast<TaggedActivity<SubActivityTag>&>(result);
|
||||
// return reinterpret_cast<TaggedActivity<SubActivityTag>&>(*std::next(activity.sub_activities.begin(), index));
|
||||
}
|
||||
|
||||
// template<typename FirstTag, typename SecondTag, typename... SubActivityTags>
|
||||
// auto GetSubActivity() {
|
||||
// auto& sub_activity = GetSubActivity<FirstTag>();
|
||||
// return sub_activity.template GetSubActivity<SecondTag, SubActivityTags...>();
|
||||
// }
|
||||
|
||||
Activity& GetSubActivity(std::string_view name) {
|
||||
return activity.GetSubActivity(name);
|
||||
}
|
||||
};
|
||||
|
||||
namespace Activities {
|
||||
|
||||
using std::string_view_literals::operator""sv;
|
||||
|
||||
template<typename... SubActivities>
|
||||
struct ActivityTag {
|
||||
// subset of child activities that can be looked up at compile-time using their type rather than a runtime name
|
||||
using tags = boost::mp11::mp_list<SubActivities...>;
|
||||
};
|
||||
|
||||
struct OS : ActivityTag<> { static constexpr auto name = "OS"sv; };
|
||||
|
||||
struct Loader : ActivityTag<> { static constexpr auto name = "Loader"sv; };
|
||||
struct Shader : ActivityTag<> { static constexpr auto name = "Shader"sv; };
|
||||
struct InputAssembly : ActivityTag<> { static constexpr auto name = "InputAssembly"sv; };
|
||||
|
||||
struct VertexLoader : ActivityTag<Loader, Shader, InputAssembly> { static constexpr auto name = "VertexLoader"sv; };
|
||||
|
||||
struct SubmitBatch : ActivityTag<VertexLoader> { static constexpr auto name = "SubmitBatch"sv; };
|
||||
|
||||
struct GPU : ActivityTag<SubmitBatch> { static constexpr auto name = "GPU"sv; };
|
||||
|
||||
struct Root : ActivityTag<OS, GPU> { static constexpr auto name = "root"sv; };
|
||||
|
||||
} // namespace Activities
|
||||
|
||||
class Profiler {
|
||||
/*
|
||||
struct Timeline {
|
||||
// TODO: Keep list of events/tasks: std::vector<START, std::variant<Event, Task*>>
|
||||
Task* active_task;
|
||||
};
|
||||
*/
|
||||
|
||||
// TODO: Keep separate root Activity per thread!
|
||||
// Activity root_activity;
|
||||
TaggedActivity<Activities::Root> root_activity;
|
||||
|
||||
public:
|
||||
Profiler() {
|
||||
root_activity->Resume();
|
||||
}
|
||||
|
||||
Activity& GetActivity(std::string_view name) {
|
||||
return root_activity->GetSubActivity(name);
|
||||
}
|
||||
|
||||
// Get a snapshot of the metrics for all activities
|
||||
// Thread-safe.
|
||||
FrozenMetrics Freeze() const;
|
||||
|
||||
// TODO: Activity GetActive() => Should be able to dynamically add a sub activity for the current one!
|
||||
};
|
||||
|
||||
// Resumes the subactivity for the current scope; at scope exit, the entire activity is interrupted
|
||||
// This function may be called with multiple sub category strings, e.g. MeasureScope(activity, "SubTask", "SubSubTask")
|
||||
template<typename... SubCategories>
|
||||
inline auto MeasureScope(Activity& activity, SubCategories&&... subs) {
|
||||
struct ScopeGuard {
|
||||
Activity& activity;
|
||||
|
||||
~ScopeGuard() noexcept(false) {
|
||||
// If there are uncaught exceptions, we probably end up in an unexpected category.
|
||||
// Just abort profiling in this case hence
|
||||
if (!std::uncaught_exceptions()) {
|
||||
activity.Interrupt();
|
||||
}
|
||||
}
|
||||
};
|
||||
// Successively iterate into sub categories based on the given strings
|
||||
Activity* sub_activity = &activity;
|
||||
((sub_activity = &sub_activity->GetSubActivity(std::forward<SubCategories>(subs))), ...);
|
||||
sub_activity->Resume();
|
||||
|
||||
// On scope exit, interrupt the parent activity
|
||||
return ScopeGuard { activity };
|
||||
}
|
||||
|
||||
} // namespace Profiler
|
16
source/framework/ranges.hpp
Normal file
16
source/framework/ranges.hpp
Normal file
|
@ -0,0 +1,16 @@
|
|||
#pragma once
|
||||
|
||||
#include <range/v3/view/iota.hpp>
|
||||
#include <iterator> // for std::size of C arrays
|
||||
|
||||
namespace ranges::views {
|
||||
|
||||
/// Returns a view of indexes into the given range
|
||||
template<random_access_range Rng>
|
||||
inline auto indexes(Rng&& rng) noexcept {
|
||||
using std::size;
|
||||
using index_t = decltype(size(rng));
|
||||
return ranges::views::iota(index_t { 0 }, size(rng));
|
||||
}
|
||||
|
||||
}
|
105
source/framework/settings.hpp
Normal file
105
source/framework/settings.hpp
Normal file
|
@ -0,0 +1,105 @@
|
|||
#pragma once
|
||||
|
||||
#include "framework/config_framework.hpp"
|
||||
|
||||
#include <boost/variant.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <variant>
|
||||
|
||||
namespace Settings {
|
||||
|
||||
enum class CPUEngine {
|
||||
NARMive,
|
||||
};
|
||||
|
||||
struct CPUEngineTag : Config::Option {
|
||||
static constexpr const char* name = "CPUEngine";
|
||||
using type = CPUEngine;
|
||||
static type default_value() { return default_val; }
|
||||
|
||||
static CPUEngine default_val;
|
||||
};
|
||||
|
||||
struct InitialApplicationTag : Config::Option {
|
||||
|
||||
struct HostFile {
|
||||
std::string filename;
|
||||
};
|
||||
|
||||
struct FileDescriptor {
|
||||
int fd;
|
||||
};
|
||||
|
||||
static constexpr const char* name = "InitialApplication";
|
||||
using type = std::variant<std::monostate, HostFile, FileDescriptor>;
|
||||
static type default_value() { return std::monostate {}; }
|
||||
};
|
||||
|
||||
// Launches the Home Menu (or other menu with the given title ID) from emulated NAND upon OS startup
|
||||
struct BootToHomeMenu : Config::BooleanOption<BootToHomeMenu> {
|
||||
static constexpr const char* name = "BootToHomeMenu";
|
||||
};
|
||||
|
||||
// Use the native HID module upon OS startup
|
||||
struct UseNativeHID : Config::BooleanOption<UseNativeHID> {
|
||||
static constexpr const char* name = "UseNativeHID";
|
||||
};
|
||||
|
||||
// Use the native FS module upon OS startup
|
||||
struct UseNativeFS : Config::BooleanOption<UseNativeFS> {
|
||||
static constexpr const char* name = "UseNativeFS";
|
||||
};
|
||||
|
||||
// Dump displayed frames to a series of binary files
|
||||
struct DumpFrames : Config::BooleanOption<DumpFrames> {
|
||||
static constexpr const char* name = "DumpFrames";
|
||||
};
|
||||
|
||||
// Connect to a debugger via the GDB remote protocol on startup
|
||||
struct ConnectToDebugger : Config::BooleanOption<ConnectToDebugger> {
|
||||
static constexpr const char* name = "ConnectToDebugger";
|
||||
};
|
||||
|
||||
// Connect to a debugger via the GDB remote protocol on startup
|
||||
struct AttachToProcessOnStartup : Config::IntegralOption<unsigned, AttachToProcessOnStartup> {
|
||||
static constexpr const char* name = "ProcessToAttachTo";
|
||||
};
|
||||
|
||||
struct AppMemType : Config::IntegralOption<unsigned, AppMemType> {
|
||||
static constexpr const char* name = "AppMemType";
|
||||
};
|
||||
|
||||
enum class Renderer {
|
||||
Software,
|
||||
Vulkan,
|
||||
};
|
||||
|
||||
struct RendererTag : Config::OptionDefault<Renderer> {
|
||||
static constexpr const char* name = "Renderer";
|
||||
};
|
||||
|
||||
enum class ShaderEngine {
|
||||
Interpreter,
|
||||
Bytecode,
|
||||
GLSL,
|
||||
};
|
||||
|
||||
struct ShaderEngineTag : Config::OptionDefault<ShaderEngine> {
|
||||
static constexpr const char* name = "ShaderEngine";
|
||||
};
|
||||
|
||||
|
||||
struct Settings : Config::Options<CPUEngineTag,
|
||||
InitialApplicationTag,
|
||||
BootToHomeMenu,
|
||||
UseNativeHID,
|
||||
UseNativeFS,
|
||||
DumpFrames,
|
||||
ConnectToDebugger,
|
||||
AttachToProcessOnStartup,
|
||||
AppMemType,
|
||||
RendererTag,
|
||||
ShaderEngineTag> { };
|
||||
|
||||
} // namespace Settings
|
146
source/framework/tests/bit_field.cpp
Normal file
146
source/framework/tests/bit_field.cpp
Normal file
|
@ -0,0 +1,146 @@
|
|||
#include <framework/bit_field_new.hpp>
|
||||
|
||||
#include <catch2/catch.hpp>
|
||||
|
||||
// TODO: Integrate into cmake
|
||||
#include <boost/endian/arithmetic.hpp>
|
||||
|
||||
using namespace BitField::v3;
|
||||
|
||||
enum MyUnscopedEnum {
|
||||
MyUnscopedEnumValue_0x45 = 0x45,
|
||||
MyUnscopedEnumValue_0xab = 0xab,
|
||||
};
|
||||
|
||||
enum class MyScopedEnum {
|
||||
Value_0x23 = 0x23,
|
||||
Value_0xcd = 0xcd,
|
||||
};
|
||||
|
||||
struct OneElementStruct {
|
||||
uint8_t element;
|
||||
};
|
||||
|
||||
constexpr bool operator==(const OneElementStruct& a, const OneElementStruct& b) {
|
||||
return a.element == b.element;
|
||||
}
|
||||
|
||||
struct OneElementStructWithNonDefaultConstructor {
|
||||
explicit constexpr OneElementStructWithNonDefaultConstructor(uint8_t value) : element(value) {
|
||||
}
|
||||
|
||||
uint8_t element;
|
||||
};
|
||||
|
||||
constexpr bool operator==(const OneElementStructWithNonDefaultConstructor& a, const OneElementStructWithNonDefaultConstructor& b) {
|
||||
return a.element == b.element;
|
||||
}
|
||||
|
||||
struct Test {
|
||||
uint_least32_t value;
|
||||
|
||||
constexpr auto full_value() const { return MakeFieldOn<0, 32>(this); }
|
||||
|
||||
constexpr auto byte0() const { return MakeFieldOn<0, 8>(this); }
|
||||
constexpr auto byte1() const { return MakeFieldOn<8, 8>(this); }
|
||||
constexpr auto byte2() const { return MakeFieldOn<16, 8>(this); }
|
||||
constexpr auto byte3() const { return MakeFieldOn<24, 8>(this); }
|
||||
|
||||
// Boolean flags
|
||||
constexpr auto flag0() const { return MakeFlagOn<24>(this); }
|
||||
constexpr auto flag1() const { return MakeFlagOn<25>(this); }
|
||||
|
||||
// Signed values
|
||||
constexpr auto byte3_signed() const { return MakeFieldOn<24, 8, int8_t>(this); }
|
||||
|
||||
// Different exposures
|
||||
constexpr auto byte1_as_unscoped_enum() const { return MakeFieldOn<8, 8, MyUnscopedEnum>(this); }
|
||||
constexpr auto byte2_as_scoped_enum() const { return MakeFieldOn<16, 8, MyScopedEnum>(this); }
|
||||
constexpr auto byte3_as_struct() const { return MakeFieldOn<24, 8, OneElementStruct>(this); }
|
||||
constexpr auto byte0_as_nondefault_constructed_struct() const { return MakeFieldOn<0, 8, OneElementStructWithNonDefaultConstructor>(this); }
|
||||
};
|
||||
|
||||
// TODO: Can BitField support using boost::endian::little_uintX_t as Storage?
|
||||
|
||||
TEST_CASE("BitField") {
|
||||
// TODO: Access through plain integral value
|
||||
|
||||
SECTION("Simple access through parent structure")
|
||||
{
|
||||
constexpr auto test = Test { 0x1234567 };
|
||||
|
||||
// Read bit fields
|
||||
static_assert(test.full_value() == 0x1234567);
|
||||
|
||||
static_assert(test.byte0() == 0x67);
|
||||
static_assert(test.byte1() == 0x45);
|
||||
static_assert(test.byte2() == 0x23);
|
||||
static_assert(test.byte3() == 0x01);
|
||||
|
||||
static_assert(test.byte3_signed() == 1);
|
||||
|
||||
static_assert(test.flag0());
|
||||
static_assert(!test.flag1());
|
||||
|
||||
static_assert(test.byte1_as_unscoped_enum() == MyUnscopedEnumValue_0x45);
|
||||
static_assert(test.byte2_as_scoped_enum() == MyScopedEnum::Value_0x23);
|
||||
static_assert(test.byte3_as_struct() == OneElementStruct { 0x01 });
|
||||
static_assert(test.byte0_as_nondefault_constructed_struct() == OneElementStructWithNonDefaultConstructor { 0x67 });
|
||||
|
||||
|
||||
// Create new value using bit field modifiers
|
||||
{
|
||||
constexpr auto modified_test = test.byte0()(0x89).byte1()(0xab).byte2()(0xcd).byte3()(0xef);
|
||||
static_assert(std::is_same_v<decltype(modified_test), const Test>); // Ensure the return value is of the input structure type
|
||||
|
||||
static_assert(modified_test.full_value() == 0xefcdab89);
|
||||
|
||||
static_assert(modified_test.byte0() == 0x89);
|
||||
static_assert(modified_test.byte1() == 0xab);
|
||||
static_assert(modified_test.byte2() == 0xcd);
|
||||
static_assert(modified_test.byte3() == 0xef);
|
||||
|
||||
static_assert(modified_test.byte3_signed() == -17);
|
||||
|
||||
static_assert(modified_test.flag0());
|
||||
static_assert(modified_test.flag1());
|
||||
|
||||
static_assert(modified_test.byte1_as_unscoped_enum() == MyUnscopedEnumValue_0xab);
|
||||
static_assert(modified_test.byte2_as_scoped_enum() == MyScopedEnum::Value_0xcd);
|
||||
static_assert(modified_test.byte3_as_struct() == OneElementStruct { 0xef });
|
||||
static_assert(modified_test.byte0_as_nondefault_constructed_struct() == OneElementStructWithNonDefaultConstructor { 0x89 });
|
||||
}
|
||||
|
||||
// Modify via signed integer
|
||||
// NOTE: Signed integers are two's complement
|
||||
{
|
||||
static_assert(test.byte3_signed()(-128).byte3_signed() == -128);
|
||||
static_assert(test.byte3_signed()(-127).byte3_signed() == -127);
|
||||
static_assert(test.byte3_signed()(127).byte3_signed() == 127);
|
||||
static_assert(test.byte3_signed()(0).byte3_signed() == 0);
|
||||
|
||||
static_assert(test.byte3_signed()(-128).full_value() == 0x80234567);
|
||||
static_assert(test.byte3_signed()(-127).full_value() == 0x81234567);
|
||||
static_assert(test.byte3_signed()(127).full_value() == 0x7f234567);
|
||||
static_assert(test.byte3_signed()(0).full_value() == 0x0234567);
|
||||
}
|
||||
|
||||
// Modify via flags
|
||||
{
|
||||
constexpr auto modified_test = test.flag0()(!test.flag0()).flag1()(!test.flag1());
|
||||
static_assert(modified_test.full_value() == 0x2234567);
|
||||
static_assert(!modified_test.flag0());
|
||||
static_assert(modified_test.flag1());
|
||||
}
|
||||
|
||||
// Modify via enums
|
||||
{
|
||||
constexpr auto modified_test = test.byte1_as_unscoped_enum()(MyUnscopedEnumValue_0xab).byte2_as_scoped_enum()(MyScopedEnum::Value_0xcd);
|
||||
static_assert(modified_test.full_value() == 0x1cdab67);
|
||||
static_assert(modified_test.byte1_as_unscoped_enum() == MyUnscopedEnumValue_0xab);
|
||||
static_assert(modified_test.byte2_as_scoped_enum() == MyScopedEnum::Value_0xcd);
|
||||
}
|
||||
|
||||
// TODO: Also support modifying OneElementStruct and OneElementStructWithNonDefaultConstructor
|
||||
}
|
||||
}
|
47
source/framework/tests/config.cpp
Normal file
47
source/framework/tests/config.cpp
Normal file
|
@ -0,0 +1,47 @@
|
|||
#include "framework/config_framework.hpp"
|
||||
|
||||
#include <catch2/catch.hpp>
|
||||
|
||||
#include <string>
|
||||
|
||||
struct IntegerTag : Config::Option {
|
||||
static constexpr const char* name = "IntegerTag";
|
||||
using type = int;
|
||||
static type default_value() { return -1; }
|
||||
};
|
||||
|
||||
struct BooleanOptionTagTrue : Config::BooleanOption<BooleanOptionTagTrue> {
|
||||
static constexpr const char* name = "BooleanTagTrue";
|
||||
};
|
||||
|
||||
struct BooleanOptionTagFalse : Config::BooleanOption<BooleanOptionTagFalse> {
|
||||
static constexpr const char* name = "BooleanTagFalse";
|
||||
};
|
||||
|
||||
template<>
|
||||
bool Config::BooleanOption<BooleanOptionTagTrue>::default_val = true;
|
||||
template<>
|
||||
bool Config::BooleanOption<BooleanOptionTagFalse>::default_val = false;
|
||||
|
||||
struct StructTag : Config::Option {
|
||||
struct Data {
|
||||
std::string string;
|
||||
bool operator == (const Data& d) const {
|
||||
return string == d.string;
|
||||
}
|
||||
};
|
||||
static constexpr const char* name = "StructTag";
|
||||
using type = Data;
|
||||
static type default_value() { return {"Hello World"}; }
|
||||
};
|
||||
|
||||
using Settings = Config::Options<IntegerTag, BooleanOptionTagTrue, BooleanOptionTagFalse, StructTag>;
|
||||
|
||||
TEST_CASE("Default Settings") {
|
||||
Settings settings;
|
||||
|
||||
REQUIRE(settings.get<IntegerTag>() == -1);
|
||||
REQUIRE(settings.get<BooleanOptionTagTrue>() == true);
|
||||
REQUIRE(settings.get<BooleanOptionTagFalse>() == false);
|
||||
REQUIRE(settings.get<StructTag>() == StructTag::Data { "Hello World" });
|
||||
}
|
2
source/framework/tests/test_main.cpp
Normal file
2
source/framework/tests/test_main.cpp
Normal file
|
@ -0,0 +1,2 @@
|
|||
#define CATCH_CONFIG_MAIN
|
||||
#include <catch2/catch.hpp>
|
1185
source/gdb_stub.cpp
Normal file
1185
source/gdb_stub.cpp
Normal file
File diff suppressed because it is too large
Load diff
93
source/gdb_stub.h
Normal file
93
source/gdb_stub.h
Normal file
|
@ -0,0 +1,93 @@
|
|||
#pragma once
|
||||
|
||||
#include "interpreter.h"
|
||||
|
||||
#include <spdlog/common.h>
|
||||
#include <spdlog/logger.h>
|
||||
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
class LogManager;
|
||||
|
||||
namespace HLE {
|
||||
namespace OS {
|
||||
class OS;
|
||||
class Thread;
|
||||
class Process;
|
||||
|
||||
using ProcessId = uint32_t;
|
||||
using ThreadId = uint32_t;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
namespace Interpreter {
|
||||
|
||||
class GDBStubTCPServer;
|
||||
class GDBStub final : public ProcessorController {
|
||||
friend class GDBStubTCPServer;
|
||||
|
||||
// We refer to the "OS" more abstractly as env(ironment) here.
|
||||
// In the future, we may support LLE kernel emulation, and as such
|
||||
// may only have one thread remaining to debug. To simplify design of
|
||||
// this stub interface, we might still want to keep around a virtual
|
||||
// OS layer around at that point, though.
|
||||
HLE::OS::OS& env;
|
||||
|
||||
std::unique_ptr<GDBStubTCPServer> server;
|
||||
|
||||
std::unordered_map<char, std::pair<HLE::OS::ProcessId,HLE::OS::ThreadId>> thread_for_operation;
|
||||
|
||||
std::shared_ptr<spdlog::logger> logger;
|
||||
|
||||
std::vector<HLE::OS::ProcessId> attached_processes;
|
||||
|
||||
// Information about a process that is waiting for a debugger to attach
|
||||
std::weak_ptr<WrappedAttachInfo> attach_info;
|
||||
|
||||
/**
|
||||
* Handle an incoming GDB command
|
||||
*
|
||||
* Returns an optional response to send to GDB.
|
||||
*/
|
||||
std::optional<std::string> HandlePacket(const std::string& command);
|
||||
|
||||
void Continue(bool single_step = false);
|
||||
|
||||
/**
|
||||
* Get CPU Id of the controlled CPU
|
||||
*/
|
||||
unsigned GetCPUId();
|
||||
|
||||
void OnSegfault(uint32_t process_id, uint32_t thread_id) override;
|
||||
void OnBreakpoint(uint32_t process_id, uint32_t thread_id) override;
|
||||
void OnReadWatchpoint(uint32_t process_id, uint32_t thread_id, uint32_t data_address) override;
|
||||
void OnWriteWatchpoint(uint32_t process_id, uint32_t thread_id, uint32_t data_address) override;
|
||||
void OnProcessLaunched(uint32_t process_id, uint32_t thread_id, uint32_t launched_process_id) override;
|
||||
|
||||
// Environment inspection utility functions
|
||||
|
||||
// Get currently active thread
|
||||
HLE::OS::Thread& GetCurrentThread() const;
|
||||
|
||||
// Get currently active process
|
||||
HLE::OS::Process& GetCurrentProcess() const;
|
||||
|
||||
// Get a list of threads running in the environment
|
||||
std::vector<std::shared_ptr<HLE::OS::Thread>> GetThreadList() const;
|
||||
|
||||
std::shared_ptr<HLE::OS::Thread> GetThreadForOperation(char operation);
|
||||
|
||||
public:
|
||||
GDBStub(HLE::OS::OS& os, LogManager& log_manager, unsigned port);
|
||||
~GDBStub();
|
||||
|
||||
void OfferAttach(std::weak_ptr<WrappedAttachInfo> attach_info) override;
|
||||
|
||||
void ProcessQueue();
|
||||
};
|
||||
|
||||
} // namespace
|
1014
source/gui-sdl/main.cpp
Normal file
1014
source/gui-sdl/main.cpp
Normal file
File diff suppressed because it is too large
Load diff
510
source/gui-sdl/sdl_vulkan_display.hpp
Normal file
510
source/gui-sdl/sdl_vulkan_display.hpp
Normal file
|
@ -0,0 +1,510 @@
|
|||
#include <thread>
|
||||
#include "pica.hpp"
|
||||
#include "../video_core/src/video_core/vulkan/renderer.hpp" // TODO: Get rid of this
|
||||
#include "../video_core/src/video_core/vulkan/layout_transitions.hpp" // TODO: Get rid of this
|
||||
#include "../video_core/src/video_core/vulkan/awaitable.hpp" // TODO: Get rid of this
|
||||
|
||||
#include <vulkan_utils/device_manager.hpp>
|
||||
|
||||
#include <vulkan/vulkan.hpp>
|
||||
|
||||
#include <spdlog/logger.h>
|
||||
|
||||
#include <SDL_render.h>
|
||||
#include <SDL_video.h>
|
||||
#include <SDL_vulkan.h>
|
||||
|
||||
#include <vulkan/vulkan.hpp>
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <iostream> // TODO
|
||||
#include <iomanip> // TODO
|
||||
|
||||
#include <range/v3/algorithm/fill.hpp>
|
||||
#include <range/v3/view/iota.hpp>
|
||||
|
||||
// Must be included after range-v3 because of colliding definitions
|
||||
#include <SDL_syswm.h>
|
||||
|
||||
inline constexpr bool enable_framedump = false;
|
||||
|
||||
// Layout of 3DS screens as arranged on the host display
|
||||
struct Layout {
|
||||
bool enabled;
|
||||
unsigned x, y; // top-left corner
|
||||
unsigned width, height; // Displayed size (stretched if needed)
|
||||
};
|
||||
|
||||
class SDLVulkanDisplay : public VulkanInstanceManager, public VulkanDeviceManager, public EmuDisplay::EmuDisplay {
|
||||
public: // ... TODO
|
||||
std::shared_ptr<spdlog::logger> logger;
|
||||
|
||||
SDL_Window& window;
|
||||
std::optional<VkSurfaceKHR> surface; // TODO: Make this a UniqueHandle instead, and point it to the instance (otherwise its destructor will crash, apparently). ALSO, REORDER TO AFTER THE INSTANCE!!!
|
||||
|
||||
const std::array<Layout, 3>& layouts; // TODO: use num_screen_ids instead of a hardcoded constant
|
||||
|
||||
vk::UniqueSwapchainKHR swapchain;
|
||||
std::vector<vk::Image> swapchain_images;
|
||||
std::vector<vk::ImageLayout> swapchain_image_layouts;
|
||||
|
||||
vk::UniqueCommandPool command_pool;
|
||||
vk::CommandBuffer* command_buffer = nullptr; // TODO: Get rid of this
|
||||
|
||||
struct FrameData {
|
||||
static constexpr uint32_t num_screen_ids = 3;
|
||||
static auto ScreenIds() { return ranges::view::iota(uint32_t { 0 }, num_screen_ids); }
|
||||
|
||||
vk::UniqueSemaphore render_finished_semaphore;
|
||||
vk::UniqueFence render_finished_fence;
|
||||
CPUAwaitable render_finished_fence_awaitable {}; // TODO: Have a helper type that manages the lifetime of the managed fence!
|
||||
|
||||
std::array<bool, num_screen_ids> screen_active {};
|
||||
|
||||
vk::UniqueCommandBuffer command_buffer;
|
||||
|
||||
uint32_t image_index; // swap chain image index
|
||||
vk::UniqueSemaphore image_available_semaphore;
|
||||
|
||||
vk::UniqueBuffer framedump_buffer;
|
||||
vk::UniqueDeviceMemory framedump_memory;
|
||||
};
|
||||
|
||||
// // Data currently shown to the screen. Kept around so we can display it again if the emulation core doesn't provide another image in time
|
||||
// std::array<std::shared_ptr<::EmuDisplay::VulkanDataStream>, FrameData::num_screen_ids> active_images;
|
||||
|
||||
std::vector<FrameData> frame_data;
|
||||
std::vector<FrameData>::iterator current_frame;
|
||||
|
||||
static std::vector<const char*> GetRequiredExtensions(SDL_Window& window) {
|
||||
unsigned count;
|
||||
if (!SDL_Vulkan_GetInstanceExtensions(&window, &count, nullptr)) {
|
||||
throw std::runtime_error("Failed to query Vulkan extensions required by SDL");
|
||||
}
|
||||
|
||||
std::vector<const char*> extensions(count);
|
||||
if (!SDL_Vulkan_GetInstanceExtensions(&window, &count, extensions.data())) {
|
||||
throw std::runtime_error("Failed to query Vulkan extensions required by SDL");
|
||||
}
|
||||
extensions.push_back("VK_EXT_debug_report");
|
||||
|
||||
return extensions;
|
||||
}
|
||||
|
||||
struct UniqueSurfaceKHR {
|
||||
vk::Instance instance;
|
||||
vk::SurfaceKHR surface { };
|
||||
|
||||
~UniqueSurfaceKHR() {
|
||||
if (surface) {
|
||||
instance.destroySurfaceKHR(surface);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static UniqueSurfaceKHR CreatePresentSurface(vk::Instance instance, SDL_Window& window) {
|
||||
VkSurfaceKHR surface;
|
||||
if (!SDL_Vulkan_CreateSurface(&window, instance, &surface)) {
|
||||
throw std::runtime_error("Failed to create SDL surface");
|
||||
}
|
||||
return { instance, surface };
|
||||
}
|
||||
|
||||
void ProcessDataSource(::EmuDisplay::Frame& frame, ::EmuDisplay::DataStreamId stream_id) {
|
||||
#ifdef __ANDROID__
|
||||
// Right-eye frames are always ignored on Android
|
||||
if (input->stream_id == ::Display::DataStreamId::TopScreenRightEye) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
// {
|
||||
// auto& active_image = active_images[static_cast<size_t>(input->stream_id)];
|
||||
// if (active_image && active_image != input) {
|
||||
// // Get previous frame, then link its semaphore against the VulkanDataStream fence
|
||||
// // TODO: Find a cleaner way of signaling these.
|
||||
// // TODO: Does this work when only a single swapchain image is used?
|
||||
|
||||
// active_image->in_use_by_gpu = GetPreviousFrame()->render_finished_fence_awaitable;
|
||||
|
||||
// active_image->in_use_by_cpu = false;
|
||||
// }
|
||||
// active_image = input;
|
||||
// }
|
||||
|
||||
// TODO: Don't hardcode
|
||||
uint32_t image_width = stream_id == ::EmuDisplay::DataStreamId::BottomScreen ? 320 : 400;
|
||||
uint32_t image_height = 240;
|
||||
|
||||
if (enable_framedump) { // TODO: Hide behind setting
|
||||
// TODO: Synch presentation mode only (?)
|
||||
vk::BufferImageCopy buffer_image_copy {
|
||||
400 * 240 * 4 * static_cast<size_t>(stream_id), // TODO: proper offset
|
||||
0,
|
||||
0,
|
||||
vk::ImageSubresourceLayers { vk::ImageAspectFlagBits::eColor, 0, 0, 1 },
|
||||
vk::Offset3D { },
|
||||
vk::Extent3D { image_width, image_height, 1 }
|
||||
};
|
||||
|
||||
// TODO: Pipeline barrier to wait for rendering to finish
|
||||
|
||||
// vk::ImageSubresourceRange full_image = { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 };
|
||||
// TransitionImageLayout(command_buffer, input->image, full_image, ImageLayoutTransitionPoint::From(vk::ImageLayout::eGeneral), ImageLayoutTransitionPoint::ToTransferSrc());
|
||||
|
||||
command_buffer->copyImageToBuffer(*frame.image, vk::ImageLayout::eGeneral/*eTransferSrcOptimal*/, *current_frame->framedump_buffer, { buffer_image_copy });
|
||||
|
||||
// auto target_transition_point = ImageLayoutTransitionPoint::ToColorAttachmentOutput();
|
||||
// TransitionImageLayout(command_buffer, input->image, full_image,
|
||||
// ImageLayoutTransitionPoint::FromTransferSrc(), target_transition_point);
|
||||
}
|
||||
|
||||
{
|
||||
auto& layout = layouts[static_cast<size_t>(stream_id)];
|
||||
|
||||
auto full_image = vk::ImageSubresourceLayers { vk::ImageAspectFlagBits::eColor, 0, 0, 1 };
|
||||
auto offset = vk::Offset3D { static_cast<int32_t>(layout.x), static_cast<int32_t>(layout.y), 0 };
|
||||
vk::ImageBlit copy_region {
|
||||
full_image,
|
||||
{ vk::Offset3D { 0, 0, 0}, vk::Offset3D { static_cast<int32_t>(image_width), static_cast<int32_t>(image_height), 1 } },
|
||||
full_image,
|
||||
{ offset, vk::Offset3D { static_cast<int32_t>(layout.x + layout.width), static_cast<int32_t>(layout.y + layout.height), 1 } }
|
||||
};
|
||||
|
||||
auto swapchain_image = swapchain_images[current_frame->image_index];
|
||||
auto& swapchain_image_layout = swapchain_image_layouts[current_frame->image_index];
|
||||
if (swapchain_image_layout != vk::ImageLayout::eTransferDstOptimal) {
|
||||
throw std::runtime_error("Image should be in transfer dst layout");
|
||||
}
|
||||
if (layouts[static_cast<size_t>(stream_id)].enabled) {
|
||||
command_buffer->blitImage(*frame.image, vk::ImageLayout::eGeneral, swapchain_image, vk::ImageLayout::eTransferDstOptimal, { copy_region }, vk::Filter::eLinear);
|
||||
}
|
||||
}
|
||||
current_frame->screen_active[static_cast<size_t>(stream_id)] = true;
|
||||
}
|
||||
|
||||
static bool IsRunningOnWayland(SDL_Window& window) {
|
||||
SDL_SysWMinfo info {};
|
||||
SDL_VERSION(&info.version);
|
||||
SDL_GetWindowWMInfo(&window, &info);
|
||||
return (info.subsystem == SDL_SYSWM_WAYLAND);
|
||||
}
|
||||
|
||||
public:
|
||||
SDLVulkanDisplay(std::shared_ptr<spdlog::logger> logger, SDL_Window& window, const std::array<Layout, FrameData::num_screen_ids>& layouts)
|
||||
: VulkanInstanceManager(*logger, app_name, GetRequiredExtensions(window)),
|
||||
VulkanDeviceManager(*this, *logger, CreatePresentSurface(*instance, window).surface, IsRunningOnWayland(window)),
|
||||
EmuDisplay(*logger, physical_device, *device, graphics_queue_index),
|
||||
logger(logger), window(window), layouts(layouts) {
|
||||
|
||||
CreateSwapchain();
|
||||
|
||||
// NOTE: eTransient will be very interesting for us, but in the actual rendering thread
|
||||
|
||||
{
|
||||
vk::CommandPoolCreateInfo info { vk::CommandPoolCreateFlagBits::eResetCommandBuffer, graphics_queue_index };
|
||||
command_pool = device->createCommandPoolUnique(info);
|
||||
}
|
||||
|
||||
frame_data.resize(swapchain_images.size());
|
||||
for (auto& frame : frame_data) {
|
||||
vk::CommandBufferAllocateInfo info { *command_pool, vk::CommandBufferLevel::ePrimary, FrameData::num_screen_ids };
|
||||
frame.command_buffer = std::move(device->allocateCommandBuffersUnique(info)[0]);
|
||||
|
||||
frame.image_available_semaphore = device->createSemaphoreUnique(vk::SemaphoreCreateInfo { });
|
||||
|
||||
frame.render_finished_semaphore = device->createSemaphoreUnique(vk::SemaphoreCreateInfo { });
|
||||
|
||||
frame.render_finished_fence = device->createFenceUnique(vk::FenceCreateInfo { vk::FenceCreateFlagBits::eSignaled });
|
||||
|
||||
vk::BufferCreateInfo buffer_info {
|
||||
vk::BufferCreateFlagBits { },
|
||||
400 * 240 * 4 * FrameData::num_screen_ids, /* TODO: Proper size */
|
||||
vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eTransferDst,
|
||||
vk::SharingMode::eExclusive,
|
||||
};
|
||||
frame.framedump_buffer = device->createBufferUnique(buffer_info);
|
||||
|
||||
// TODO: Generalize memory type selection
|
||||
frame.framedump_memory = device->allocateMemoryUnique(vk::MemoryAllocateInfo { buffer_info.size, /*1*/ 2 });
|
||||
device->bindBufferMemory(*frame.framedump_buffer, *frame.framedump_memory, 0);
|
||||
}
|
||||
current_frame = frame_data.begin();
|
||||
}
|
||||
|
||||
void ResetSwapchainResources() {
|
||||
swapchain = vk::UniqueSwapchainKHR{};
|
||||
swapchain_images.clear();
|
||||
|
||||
if (surface) { // TODO: Having this as an optional is kind of redundant. Instead, use a UniqueSurfaceKHR?
|
||||
logger->info("Destroying SDL surface");
|
||||
instance->destroySurfaceKHR(*surface);
|
||||
surface = {};
|
||||
}
|
||||
}
|
||||
|
||||
void CreateSwapchain() {
|
||||
ResetSwapchainResources();
|
||||
|
||||
VkSurfaceKHR surface_handle;
|
||||
|
||||
// TODO: Use a vk::UniqueHandle here instead
|
||||
if (!SDL_Vulkan_CreateSurface(&window, *instance, &surface_handle)) {
|
||||
throw std::runtime_error(fmt::format("Failed to create Vulkan SDL surface: {}", SDL_GetError()));
|
||||
}
|
||||
surface = surface_handle;
|
||||
if (!physical_device.getSurfaceSupportKHR(present_queue_index, vk::SurfaceKHR { surface_handle })) {
|
||||
throw std::runtime_error("New SDL surface does not support presentation on the previous present queue");
|
||||
}
|
||||
|
||||
auto surface_caps = physical_device.getSurfaceCapabilitiesKHR(surface_handle);
|
||||
logger->info("Surface width: {} - {}", surface_caps.minImageExtent.width, surface_caps.maxImageExtent.width);
|
||||
logger->info("Surface height: {} - {}", surface_caps.minImageExtent.height, surface_caps.maxImageExtent.height);
|
||||
logger->info("Surface image count: {} - {}", surface_caps.minImageCount, surface_caps.maxImageCount);
|
||||
if (surface_caps.minImageExtent != surface_caps.maxImageExtent) {
|
||||
logger->warn("Expected image extent bounds to be equal");
|
||||
// throw std::runtime_error("Expected image extent bounds to be equal");
|
||||
}
|
||||
logger->info("Surface supported usage flags: {}", vk::to_string(surface_caps.supportedUsageFlags));
|
||||
// TODO: Verify the surface has the eTransferDst flag
|
||||
|
||||
auto surface_formats = physical_device.getSurfaceFormatsKHR(surface_handle);
|
||||
for (auto& format : surface_formats) {
|
||||
logger->info("Surface format: {}, {}", vk::to_string(format.format), vk::to_string(format.colorSpace));
|
||||
}
|
||||
|
||||
auto surface_present_modes = physical_device.getSurfacePresentModesKHR(surface_handle);
|
||||
for (auto& mode : surface_present_modes) {
|
||||
logger->info("Present mode: {}", vk::to_string(mode));
|
||||
}
|
||||
|
||||
const uint32_t min_image_count = surface_caps.minImageCount;
|
||||
const uint32_t image_array_layers = 1;
|
||||
|
||||
// TODO: Assert this is between minImageExtent and maxImageExtent
|
||||
int window_width, window_height;
|
||||
SDL_GetWindowSize(&window, &window_width, &window_height);
|
||||
logger->info("Swap chain size: {}x{}", window_width, window_height);
|
||||
vk::Extent2D swapchain_size = { static_cast<uint32_t>(window_width), static_cast<uint32_t>(window_height) };
|
||||
#ifdef __ANDROID__
|
||||
vk::Format swapchain_format = vk::Format::eR8G8B8A8Unorm;
|
||||
#else
|
||||
vk::Format swapchain_format = vk::Format::eB8G8R8A8Unorm; // TODO: Don't hardcode. Check against surface_formats instead
|
||||
#endif
|
||||
|
||||
swapchain = std::invoke([&]() {
|
||||
vk::SwapchainCreateInfoKHR info { vk::SwapchainCreateFlagBitsKHR { },
|
||||
*surface,
|
||||
min_image_count,
|
||||
swapchain_format,
|
||||
vk::ColorSpaceKHR::eSrgbNonlinear, // TODO: Check against surface_formats
|
||||
swapchain_size,
|
||||
image_array_layers,
|
||||
vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eTransferDst,
|
||||
vk::SharingMode::eExclusive, // TODO: If the present and graphics queues are different, we should use concurrent mode here
|
||||
1, // queue family count
|
||||
&graphics_queue_index,
|
||||
vk::SurfaceTransformFlagBitsKHR::eIdentity,
|
||||
vk::CompositeAlphaFlagBitsKHR::eOpaque,
|
||||
vk::PresentModeKHR::eFifo,
|
||||
1, // clipped
|
||||
vk::SwapchainKHR { } }; // TODO: Use oldswapchain here on reset?
|
||||
return device->createSwapchainKHRUnique(info);
|
||||
});
|
||||
|
||||
swapchain_images = device->getSwapchainImagesKHR(*swapchain);
|
||||
|
||||
logger->info("Got {} swapchain images", swapchain_images.size());
|
||||
|
||||
auto full_image_range = vk::ImageSubresourceRange { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 };
|
||||
|
||||
swapchain_image_layouts.resize(swapchain_images.size(), vk::ImageLayout::eUndefined);
|
||||
|
||||
if (frame_data.size() && frame_data.size() != swapchain_images.size()) {
|
||||
throw std::runtime_error("TODO: Swapchain size changed, not implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
void BeginFrame() {
|
||||
// using clock = std::chrono::steady_clock;
|
||||
// static std::array<decltype(clock::now()), 60> last_frames;
|
||||
|
||||
// static auto BEGIN = clock::now();
|
||||
|
||||
// auto this_frame = clock::now();
|
||||
|
||||
// TODO: Commit this change!
|
||||
device->waitForFences({*current_frame->render_finished_fence}, true, std::numeric_limits<uint64_t>::max());
|
||||
|
||||
auto [result, next_image_index] = device->acquireNextImageKHR(*swapchain, std::numeric_limits<uint64_t>::max(), *current_frame->image_available_semaphore, vk::Fence { });
|
||||
if (result != vk::Result::eSuccess) {
|
||||
throw std::runtime_error(fmt::format("Unexpected error in vkAcquireNextImageKHR: {}", vk::to_string(result)));
|
||||
}
|
||||
|
||||
if (!current_frame->render_finished_fence_awaitable.IsReady(*device)) { // Reset CPUAwaitable
|
||||
throw std::runtime_error("Couldn't wait on previous UI frame");
|
||||
}
|
||||
// std::chrono::duration frame_duration = std::chrono::milliseconds { 45 };
|
||||
// // TODO: Only sleep if the difference is larger than some specific amount
|
||||
// fprintf(stderr, "SLEEPING FOR %d ms (time points %4d %4d %4d %4d %4d %4d %4d %4d %4d %4d)\n",
|
||||
// std::chrono::duration_cast<std::chrono::milliseconds>(last_frames.front() + last_frames.size() * frame_duration - this_frame).count(),
|
||||
// std::chrono::duration_cast<std::chrono::milliseconds>(last_frames[0] - BEGIN).count(),
|
||||
// std::chrono::duration_cast<std::chrono::milliseconds>(last_frames[1] - BEGIN).count(),
|
||||
// std::chrono::duration_cast<std::chrono::milliseconds>(last_frames[2] - BEGIN).count(),
|
||||
// std::chrono::duration_cast<std::chrono::milliseconds>(last_frames[3] - BEGIN).count(),
|
||||
// std::chrono::duration_cast<std::chrono::milliseconds>(last_frames[4] - BEGIN).count(),
|
||||
// std::chrono::duration_cast<std::chrono::milliseconds>(last_frames[5] - BEGIN).count(),
|
||||
// std::chrono::duration_cast<std::chrono::milliseconds>(last_frames[6] - BEGIN).count(),
|
||||
// std::chrono::duration_cast<std::chrono::milliseconds>(last_frames[7] - BEGIN).count(),
|
||||
// std::chrono::duration_cast<std::chrono::milliseconds>(last_frames[8] - BEGIN).count(),
|
||||
// std::chrono::duration_cast<std::chrono::milliseconds>(last_frames[9] - BEGIN).count());
|
||||
// std::this_thread::sleep_until(last_frames.front() + last_frames.size() * frame_duration);
|
||||
|
||||
// std::rotate(std::begin(last_frames), std::begin(last_frames) + 1, std::end(last_frames));
|
||||
// last_frames.back() = this_frame;
|
||||
|
||||
device->resetFences({*current_frame->render_finished_fence});
|
||||
current_frame->image_index = next_image_index;
|
||||
|
||||
// TODO: Somehow this is still active? Should we wait for the nextImageAcquire fence?
|
||||
command_buffer = &*current_frame->command_buffer;
|
||||
vk::CommandBufferBeginInfo info { /*vk::CommandBufferUsageFlagBits::eSimultaneousUse | */vk::CommandBufferUsageFlagBits::eOneTimeSubmit };
|
||||
command_buffer->begin(info);
|
||||
|
||||
auto& swapchain_image_layout = swapchain_image_layouts[current_frame->image_index];
|
||||
swapchain_image_layout = vk::ImageLayout::eUndefined; // TODO: Verify this is what acquireNextImageKHR resets the layout to
|
||||
auto full_image_range = vk::ImageSubresourceRange { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 };
|
||||
auto swapchain_image = swapchain_images[current_frame->image_index];
|
||||
TransitionImageLayout(*command_buffer, swapchain_image, full_image_range, ImageLayoutTransitionPoint::From(swapchain_image_layout), ImageLayoutTransitionPoint::ToTransferDst());
|
||||
swapchain_image_layout = vk::ImageLayout::eTransferDstOptimal;
|
||||
command_buffer->clearColorImage(swapchain_image, swapchain_image_layout, vk::ClearColorValue {}, { full_image_range });
|
||||
}
|
||||
|
||||
void EndFrame() {
|
||||
// TODO: Un-static-fy these
|
||||
static int counted_frames[FrameData::num_screen_ids] = {};
|
||||
static std::chrono::time_point<std::chrono::high_resolution_clock> start = std::chrono::high_resolution_clock::now();
|
||||
bool new_frame_received[FrameData::num_screen_ids] {};
|
||||
static int frame_count[FrameData::num_screen_ids] {};
|
||||
|
||||
// for (size_t screen_id = 0; screen_id < FrameData::num_screen_ids; ++screen_id) {
|
||||
// if (!active_images[screen_id]) {
|
||||
// // Ignore if no frame has ever been submitted for this screen id
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// if (!current_frame->screen_active[screen_id]) {
|
||||
// // Resubmit frames for which no new images have been provided (don't count this towards the displayed FPS, though)
|
||||
// // TODO: By allowing one frame to be "dropped" like this, we allow e.g. the right-eye frame to lag behind the left-eye frame. Instead, give dropped parts a chance to catch up!
|
||||
|
||||
// // Reset ready semaphore because we already waited for it
|
||||
//// active_images[screen_id]->is_ready.semaphore = vk::Semaphore { };
|
||||
//// TODO: Re-enable
|
||||
//// ProcessDataSource(active_images[screen_id]);
|
||||
// } else {
|
||||
// // TODO: Not reliable...
|
||||
//// auto prev_max = (counted_frames[screen_id] == std::max({counted_frames[0], counted_frames[1], counted_frames[2]}));
|
||||
// ++counted_frames[screen_id];
|
||||
//// new_frame_received = (counted_frames[screen_id] > prev_max);
|
||||
//new_frame_received[screen_id] = enable_framedump;
|
||||
// }
|
||||
// current_frame->screen_active[screen_id] = {};
|
||||
// }
|
||||
|
||||
auto& swapchain_image_layout = swapchain_image_layouts[current_frame->image_index];
|
||||
auto full_image_range = vk::ImageSubresourceRange { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 };
|
||||
auto swapchain_image = swapchain_images[current_frame->image_index];
|
||||
TransitionImageLayout(*command_buffer, swapchain_image, full_image_range, ImageLayoutTransitionPoint::FromTransferDst(), ImageLayoutTransitionPoint::ToPresent());
|
||||
swapchain_image_layout = vk::ImageLayout::ePresentSrcKHR;
|
||||
current_frame->command_buffer->end();
|
||||
|
||||
{
|
||||
// TODO: Wait for inputs' readiness GPUAwaitables
|
||||
std::array<vk::PipelineStageFlags, 1 + FrameData::num_screen_ids> wait_stages;
|
||||
ranges::fill(wait_stages, vk::PipelineStageFlagBits::eColorAttachmentOutput);
|
||||
std::vector<vk::Semaphore> wait_semaphores = { *current_frame->image_available_semaphore };
|
||||
// for (size_t screen_id = 0; screen_id < FrameData::num_screen_ids; ++screen_id) {
|
||||
// if (active_images[screen_id] && active_images[screen_id]->is_ready.semaphore) {
|
||||
// wait_semaphores.push_back(active_images[screen_id]->is_ready.semaphore);
|
||||
// }
|
||||
// }
|
||||
vk::SubmitInfo info { static_cast<uint32_t>(wait_semaphores.size()), wait_semaphores.data(),
|
||||
wait_stages.data(),
|
||||
1, &*command_buffer,
|
||||
1, &*current_frame->render_finished_semaphore };
|
||||
|
||||
graphics_queue.submit(1, &info, *current_frame->render_finished_fence);
|
||||
current_frame->render_finished_fence_awaitable = CPUAwaitable(*current_frame->render_finished_fence);
|
||||
}
|
||||
|
||||
// TODO: Consider using an Euler average here: https://github.com/dolphin-emu/dolphin/pull/10982#issuecomment-1214448160
|
||||
auto dur = std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::high_resolution_clock::now() - start);
|
||||
// if (dur.count() >= 1'000'000 * 61 / (1 + *std::max_element(std::begin(counted_frames), std::end(counted_frames))) &&
|
||||
// std::any_of(active_images.begin(), active_images.end(), [](auto& ptr) { return ptr != nullptr; })) {
|
||||
// std::string message = "Frames per second: ";
|
||||
// for (size_t screen_id = 0; screen_id < FrameData::num_screen_ids; ++screen_id) {
|
||||
// message += fmt::format("{:.3} / ", counted_frames[screen_id] * 1'000'000.f / dur.count());
|
||||
// counted_frames[screen_id] = 0;
|
||||
// }
|
||||
// message.resize(message.size() - 3); // Drop last " / "
|
||||
// logger->info(message);
|
||||
// std::cerr << message << std::endl;
|
||||
|
||||
//#ifdef __ANDROID__
|
||||
// __android_log_print(ANDROID_LOG_ERROR,
|
||||
// "MikageValidation",
|
||||
// "%s", message.c_str());
|
||||
//#endif
|
||||
|
||||
// start = std::chrono::high_resolution_clock::now();
|
||||
// }
|
||||
|
||||
vk::PresentInfoKHR info { 1, &*current_frame->render_finished_semaphore, 1, &*swapchain, ¤t_frame->image_index };
|
||||
auto result = present_queue.presentKHR(info);
|
||||
if (result != vk::Result::eSuccess) {
|
||||
// TODO: On VK_ERROR_OUT_OF_DATE_KHR, we should recreate the swap chain!
|
||||
logger->error("Error in vkQueuePresentKHR: {}", vk::to_string(result));
|
||||
throw std::runtime_error("Error in vkQueuePresentKHR");
|
||||
}
|
||||
// std::this_thread::sleep_for(std::chrono::milliseconds { 1000 } / 30);
|
||||
|
||||
if (new_frame_received[0] || new_frame_received[2]) { // TODO: Hide frame dumping behind setting
|
||||
device->waitForFences({*current_frame->render_finished_fence}, true, std::numeric_limits<uint64_t>::max());
|
||||
device->waitIdle();
|
||||
|
||||
auto data = reinterpret_cast<char*>(device->mapMemory(*current_frame->framedump_memory, 0, 400 * 240 * 4 * FrameData::num_screen_ids));
|
||||
|
||||
|
||||
if (new_frame_received[0]) {
|
||||
auto filename = fmt::format("framedump/top/framebuffer_dump_{:05}.top.data", frame_count[0]++);
|
||||
std::ofstream file(filename);
|
||||
for (unsigned y = 0; y < 240; ++y) {
|
||||
auto ptr = data + 4 * (400 * y/* + x*/);
|
||||
file.write(ptr, 400 * 4);
|
||||
}
|
||||
}
|
||||
|
||||
if (new_frame_received[2]) {
|
||||
auto filename = fmt::format("framedump/bottom/framebuffer_dump_{:05}.bottom.data", frame_count[2]++);
|
||||
std::ofstream file(filename);
|
||||
for (unsigned y = 0; y < 240; ++y) {
|
||||
auto ptr = data + 400 * 240 * 4 * 2 + 4 * (320 * y/* + x*/);
|
||||
file.write(ptr, 320 * 4);
|
||||
}
|
||||
}
|
||||
|
||||
device->unmapMemory(*current_frame->framedump_memory);
|
||||
}
|
||||
|
||||
|
||||
if (++current_frame == frame_data.end()) {
|
||||
current_frame = frame_data.begin();
|
||||
}
|
||||
}
|
||||
|
||||
~SDLVulkanDisplay() override {
|
||||
ResetSwapchainResources();
|
||||
// All other resources are cleaned up automatically
|
||||
}
|
||||
};
|
1
source/hardware/dsp.cpp
Normal file
1
source/hardware/dsp.cpp
Normal file
|
@ -0,0 +1 @@
|
|||
#include "dsp.hpp"
|
19
source/hardware/dsp.hpp
Normal file
19
source/hardware/dsp.hpp
Normal file
|
@ -0,0 +1,19 @@
|
|||
#pragma once
|
||||
|
||||
#include <framework/logging.hpp>
|
||||
|
||||
#include <memory.h>
|
||||
|
||||
#include <teakra/teakra.h>
|
||||
|
||||
namespace Memory {
|
||||
|
||||
struct DSPDevice : MemoryAccessHandler {
|
||||
std::shared_ptr<spdlog::logger> logger;
|
||||
|
||||
DSPDevice(LogManager& log_manager);
|
||||
|
||||
// Teakra::Teakra teakra;
|
||||
};
|
||||
|
||||
} // namespace Memory
|
107
source/input.cpp
Normal file
107
source/input.cpp
Normal file
|
@ -0,0 +1,107 @@
|
|||
#include "input.hpp"
|
||||
|
||||
#include <framework/bit_field_new.hpp>
|
||||
|
||||
struct ButtonState {
|
||||
uint16_t storage;
|
||||
|
||||
using Fields = v2::BitField::Fields<uint16_t>;
|
||||
|
||||
auto A() const { return Fields::MakeOn< 0, 1>(this); }
|
||||
auto B() const { return Fields::MakeOn< 1, 1>(this); }
|
||||
auto X() const { return Fields::MakeOn<10, 1>(this); }
|
||||
auto Y() const { return Fields::MakeOn<11, 1>(this); }
|
||||
auto L() const { return Fields::MakeOn< 9, 1>(this); }
|
||||
auto R() const { return Fields::MakeOn< 8, 1>(this); }
|
||||
auto Select() const { return Fields::MakeOn< 2, 1>(this); }
|
||||
auto Start() const { return Fields::MakeOn< 3, 1>(this); }
|
||||
auto DigiLeft() const { return Fields::MakeOn< 5, 1>(this); }
|
||||
auto DigiRight() const { return Fields::MakeOn< 4, 1>(this); }
|
||||
auto DigiUp() const { return Fields::MakeOn< 6, 1>(this); }
|
||||
auto DigiDown() const { return Fields::MakeOn< 7, 1>(this); }
|
||||
};
|
||||
|
||||
void InputSource::SetPressedA(bool pressed) {
|
||||
buttons = ButtonState { buttons }.A()(pressed).storage;
|
||||
}
|
||||
|
||||
void InputSource::SetPressedB(bool pressed) {
|
||||
buttons = ButtonState { buttons }.B()(pressed).storage;
|
||||
}
|
||||
|
||||
void InputSource::SetPressedX(bool pressed) {
|
||||
buttons = ButtonState { buttons }.X()(pressed).storage;
|
||||
}
|
||||
|
||||
void InputSource::SetPressedY(bool pressed) {
|
||||
buttons = ButtonState { buttons }.Y()(pressed).storage;
|
||||
}
|
||||
|
||||
void InputSource::SetPressedL(bool pressed) {
|
||||
buttons = ButtonState { buttons }.L()(pressed).storage;
|
||||
}
|
||||
|
||||
void InputSource::SetPressedR(bool pressed) {
|
||||
buttons = ButtonState { buttons }.R()(pressed).storage;
|
||||
}
|
||||
|
||||
void InputSource::SetPressedSelect(bool pressed) {
|
||||
buttons = ButtonState { buttons }.Select()(pressed).storage;
|
||||
}
|
||||
|
||||
void InputSource::SetPressedStart(bool pressed) {
|
||||
buttons = ButtonState { buttons }.Start()(pressed).storage;
|
||||
}
|
||||
|
||||
void InputSource::SetPressedDigiLeft(bool pressed) {
|
||||
buttons = ButtonState { buttons }.DigiLeft()(pressed).storage;
|
||||
}
|
||||
|
||||
void InputSource::SetPressedDigiRight(bool pressed) {
|
||||
buttons = ButtonState { buttons }.DigiRight()(pressed).storage;
|
||||
}
|
||||
|
||||
void InputSource::SetPressedDigiUp(bool pressed) {
|
||||
buttons = ButtonState { buttons }.DigiUp()(pressed).storage;
|
||||
}
|
||||
|
||||
void InputSource::SetPressedDigiDown(bool pressed) {
|
||||
buttons = ButtonState { buttons }.DigiDown()(pressed).storage;
|
||||
}
|
||||
|
||||
void InputSource::SetPressedHome(bool pressed) {
|
||||
home_button = pressed;
|
||||
}
|
||||
|
||||
auto InputSource::GetTouchState() -> TouchState {
|
||||
std::lock_guard lock(touch_mutex);
|
||||
|
||||
return touch;
|
||||
}
|
||||
|
||||
void InputSource::SetTouch(float x, float y) {
|
||||
std::lock_guard lock(touch_mutex);
|
||||
|
||||
touch.x = x;
|
||||
touch.y = y;
|
||||
touch.pressed = true;
|
||||
}
|
||||
|
||||
void InputSource::EndTouch() {
|
||||
std::lock_guard lock(touch_mutex);
|
||||
|
||||
touch.pressed = false;
|
||||
}
|
||||
|
||||
auto InputSource::GetCirclePadState() -> CirclePadState {
|
||||
std::lock_guard lock(touch_mutex);
|
||||
|
||||
return circle_pad;
|
||||
}
|
||||
|
||||
void InputSource::SetCirclePad(float x, float y) {
|
||||
std::lock_guard lock(touch_mutex);
|
||||
|
||||
circle_pad.x = x;
|
||||
circle_pad.y = y;
|
||||
}
|
76
source/input.hpp
Normal file
76
source/input.hpp
Normal file
|
@ -0,0 +1,76 @@
|
|||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
|
||||
/**
|
||||
* Interface for the frontend to set emulator-visible input state.
|
||||
*
|
||||
* Get* member functions are reserved for the emulator core.
|
||||
* Set* member functions should be called from one thread, only.
|
||||
*/
|
||||
class InputSource {
|
||||
struct TouchState {
|
||||
float x = 0.f;
|
||||
float y = 0.f;
|
||||
bool pressed = false;
|
||||
};
|
||||
|
||||
struct CirclePadState {
|
||||
float x = 0.f;
|
||||
float y = 0.f;
|
||||
};
|
||||
|
||||
std::atomic<uint16_t> buttons { 0 };
|
||||
|
||||
std::mutex touch_mutex;
|
||||
TouchState touch;
|
||||
CirclePadState circle_pad;
|
||||
|
||||
std::atomic<bool> home_button;
|
||||
|
||||
public:
|
||||
uint16_t GetButtonState() const {
|
||||
return buttons;
|
||||
}
|
||||
|
||||
TouchState GetTouchState();
|
||||
|
||||
CirclePadState GetCirclePadState();
|
||||
|
||||
bool IsHomeButtonPressed() const { return home_button; };
|
||||
|
||||
void SetPressedA(bool);
|
||||
void SetPressedB(bool);
|
||||
void SetPressedX(bool);
|
||||
void SetPressedY(bool);
|
||||
|
||||
void SetPressedL(bool);
|
||||
void SetPressedR(bool);
|
||||
|
||||
void SetPressedSelect(bool);
|
||||
void SetPressedStart(bool);
|
||||
|
||||
void SetPressedDigiLeft(bool);
|
||||
void SetPressedDigiRight(bool);
|
||||
void SetPressedDigiUp(bool);
|
||||
void SetPressedDigiDown(bool);
|
||||
|
||||
void SetPressedHome(bool);
|
||||
|
||||
/**
|
||||
* Puts touch pad into "touched" state at the given coordinates
|
||||
*
|
||||
* @param x Normalized horizontal coordinate [0..1]
|
||||
* @param y Normalized vertical coordinate [0..1]
|
||||
*/
|
||||
void SetTouch(float x, float y);
|
||||
|
||||
/**
|
||||
* Puts touch pad into "untouched" state
|
||||
*/
|
||||
void EndTouch();
|
||||
|
||||
void SetCirclePad(float x, float y);
|
||||
};
|
3414
source/interpreter.cpp
Normal file
3414
source/interpreter.cpp
Normal file
File diff suppressed because it is too large
Load diff
442
source/interpreter.h
Normal file
442
source/interpreter.h
Normal file
|
@ -0,0 +1,442 @@
|
|||
#pragma once
|
||||
|
||||
#include "arm.h"
|
||||
#include "memory.h"
|
||||
|
||||
#include <spdlog/sinks/sink.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstring>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <fstream>
|
||||
|
||||
// TODO: Shouldn't need to include this header...
|
||||
#include "loader/gamecard.hpp"
|
||||
|
||||
namespace HLE {
|
||||
namespace OS {
|
||||
class OS;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Profiler {
|
||||
class Profiler;
|
||||
}
|
||||
|
||||
namespace Debugger {
|
||||
class DebugServer;
|
||||
}
|
||||
|
||||
struct KeyDatabase;
|
||||
|
||||
class LogManager;
|
||||
|
||||
namespace Interpreter {
|
||||
|
||||
struct Breakpoint {
|
||||
uint32_t address;
|
||||
|
||||
bool operator==(const Breakpoint& bp) const {
|
||||
return address == bp.address;
|
||||
}
|
||||
};
|
||||
|
||||
struct AttachInfo {
|
||||
uint32_t pid;
|
||||
};
|
||||
|
||||
struct WrappedAttachInfo {
|
||||
// Waits until some other thread has successfully signalled request_continue via TryAccess
|
||||
void AwaitProcessing() {
|
||||
std::unique_lock<std::mutex> lock(condvar_mutex);
|
||||
request_continue.wait(lock);
|
||||
}
|
||||
|
||||
std::mutex condvar_mutex;
|
||||
std::unique_ptr<AttachInfo> data;
|
||||
|
||||
private:
|
||||
std::mutex access_mutex;
|
||||
std::condition_variable request_continue;
|
||||
|
||||
friend bool TryAccess(std::weak_ptr<WrappedAttachInfo> attach_info_weak, std::function<bool(const AttachInfo&)> eval);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tries to unwrap the AttachInfo data stored in the given weak_ptr and passes
|
||||
* it to the given callback on success.
|
||||
* @param eval Callback that should return true if the AttachInfo should be
|
||||
* permanently consumed (allowing the main thread to continue)
|
||||
* @return true if and only if the AttachInfo was consumed
|
||||
* @todo Define this elsewhere
|
||||
*/
|
||||
inline bool TryAccess(std::weak_ptr<WrappedAttachInfo> attach_info_weak, std::function<bool(const AttachInfo&)> eval) {
|
||||
auto attach_info_shared = attach_info_weak.lock();
|
||||
if (!attach_info_shared)
|
||||
return false;
|
||||
|
||||
std::lock_guard<std::mutex> guard(attach_info_shared->access_mutex);
|
||||
|
||||
auto* attach_info = attach_info_shared->data.get();
|
||||
if (!attach_info)
|
||||
return false;
|
||||
|
||||
if (!eval(*attach_info))
|
||||
return false;
|
||||
|
||||
// Drop the original data and signal the main thread to continue
|
||||
// Note that we lock the mutex here purely to make sure we don't signal the condition variable before it is being waited for
|
||||
attach_info_shared->data = nullptr;
|
||||
std::lock_guard<std::mutex>(attach_info_shared->condvar_mutex);
|
||||
attach_info_shared->request_continue.notify_all();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Controls thread-safe stepping/running/pausing/quitting of a Processor
|
||||
class ProcessorController {
|
||||
// Handlers called from ProcessEventQueue when a segfault has been reported
|
||||
virtual void OnSegfault(uint32_t process_id, uint32_t thread_id) = 0;
|
||||
virtual void OnBreakpoint(uint32_t process_id, uint32_t thread_id) = 0;
|
||||
virtual void OnProcessLaunched(uint32_t process_id, uint32_t thread_id, uint32_t launched_process_id) = 0;
|
||||
virtual void OnReadWatchpoint(uint32_t process_id, uint32_t thread_id, uint32_t data_address) = 0;
|
||||
virtual void OnWriteWatchpoint(uint32_t process_id, uint32_t thread_id, uint32_t data_address) = 0;
|
||||
|
||||
protected:
|
||||
public: // TODO: Un-public-ize!
|
||||
std::atomic<bool> request_pause{false};
|
||||
// std::atomic<bool> paused{true};
|
||||
std::atomic<bool> paused{false};
|
||||
std::atomic<bool> request_continue{false};
|
||||
|
||||
protected:
|
||||
/**
|
||||
* If valid, this denotes the process-thread-id of the thread that should
|
||||
* be paused. If boost::any is used for the thread-id (second element), any
|
||||
* thread of the given process is paused.
|
||||
*/
|
||||
std::optional<std::pair<uint32_t, std::optional<uint32_t>>> thread_to_pause;
|
||||
|
||||
friend class Processor;
|
||||
|
||||
struct Event {
|
||||
enum class Type {
|
||||
Segfault,
|
||||
Breakpoint,
|
||||
ProcessLaunched,
|
||||
ReadWatchpoint,
|
||||
WriteWatchpoint,
|
||||
} type;
|
||||
|
||||
uint32_t process_id;
|
||||
uint32_t thread_id;
|
||||
|
||||
union {
|
||||
uint32_t launched_process_id;
|
||||
uint32_t watchpoint_data_address;
|
||||
};
|
||||
};
|
||||
|
||||
std::list<Event> events;
|
||||
|
||||
void ProcessEventQueue() {
|
||||
while (!events.empty()) {
|
||||
auto& event = events.front();
|
||||
switch (event.type) {
|
||||
case Event::Type::Segfault:
|
||||
OnSegfault(event.process_id, event.thread_id);
|
||||
break;
|
||||
|
||||
case Event::Type::Breakpoint:
|
||||
OnBreakpoint(event.process_id, event.thread_id);
|
||||
break;
|
||||
|
||||
case Event::Type::ProcessLaunched:
|
||||
OnProcessLaunched(event.process_id, event.thread_id, event.launched_process_id);
|
||||
break;
|
||||
|
||||
case Event::Type::ReadWatchpoint:
|
||||
OnReadWatchpoint(event.process_id, event.thread_id, event.watchpoint_data_address);
|
||||
break;
|
||||
|
||||
case Event::Type::WriteWatchpoint:
|
||||
OnWriteWatchpoint(event.process_id, event.thread_id, event.watchpoint_data_address);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Do nothing
|
||||
;
|
||||
}
|
||||
|
||||
events.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
virtual ~ProcessorController() = default;
|
||||
|
||||
// TODO: This is probably a mess and full of race conditions... :(
|
||||
|
||||
// TODO: Need functions to signal quit requests
|
||||
|
||||
/**
|
||||
* Request the emulation core to be paused and busy-waits until the request has been fulfilled.
|
||||
* @note The amount of CPU instructions processed between the request of the pause and the actual pausing is undefined.
|
||||
*/
|
||||
void RequestPause() {
|
||||
// TODO: Get rid of this! Currently, we need it in case automated stepping encounters a breakpoint..
|
||||
if (paused)
|
||||
return;
|
||||
assert(!paused);
|
||||
|
||||
thread_to_pause = std::nullopt;
|
||||
request_pause = true;
|
||||
|
||||
while (!paused) {
|
||||
}
|
||||
|
||||
request_pause = false;
|
||||
assert(paused);
|
||||
}
|
||||
|
||||
// Request emulation to be continued without any planned pause in the future. Does not return before emulation has continued.
|
||||
void RequestContinue() {
|
||||
assert(paused);
|
||||
|
||||
request_continue = true;
|
||||
while (paused) {};
|
||||
request_continue = false;
|
||||
|
||||
assert(!paused);
|
||||
}
|
||||
|
||||
// Request emulation to be continued and pause after the next instruction. Does not return before emulation has continued and stopped again.
|
||||
void RequestStep(std::optional<std::pair<uint32_t, std::optional<uint32_t>>> process_thread_id = std::nullopt) {
|
||||
assert(paused);
|
||||
|
||||
thread_to_pause = process_thread_id;
|
||||
request_pause = true;
|
||||
request_continue = true;
|
||||
while (paused) {};
|
||||
request_continue = false;
|
||||
while (!paused) {};
|
||||
request_pause = false;
|
||||
|
||||
assert(paused);
|
||||
}
|
||||
|
||||
bool ShouldPause(uint32_t process_id, uint32_t thread_id) const {
|
||||
if (!request_pause)
|
||||
return false;
|
||||
|
||||
if (thread_to_pause == std::nullopt)
|
||||
return true;
|
||||
|
||||
if (thread_to_pause->first != process_id)
|
||||
return false;
|
||||
|
||||
return thread_to_pause->second.value_or(thread_id) == thread_id;
|
||||
}
|
||||
|
||||
// Use this in the Processor when an internal (unrecoverable) error happens
|
||||
void NotifySegfault(uint32_t process_id, uint32_t thread_id) {
|
||||
events.push_back({Event::Type::Segfault, process_id, thread_id});
|
||||
}
|
||||
|
||||
void NotifyBreakpoint(uint32_t process_id, uint32_t thread_id) {
|
||||
events.push_back({Event::Type::Breakpoint, process_id, thread_id});
|
||||
}
|
||||
|
||||
void NotifyReadWatchpoint(uint32_t process_id, uint32_t thread_id, uint32_t address) {
|
||||
events.push_back({Event::Type::ReadWatchpoint, process_id, thread_id, address});
|
||||
}
|
||||
|
||||
void NotifyWriteWatchpoint(uint32_t process_id, uint32_t thread_id, uint32_t address) {
|
||||
events.push_back({Event::Type::WriteWatchpoint, process_id, thread_id, address});
|
||||
}
|
||||
|
||||
void NotifyProcessLaunched(uint32_t process_id, uint32_t thread_id, uint32_t launched_process_id) {
|
||||
events.push_back({Event::Type::ProcessLaunched, process_id, thread_id, launched_process_id});
|
||||
}
|
||||
|
||||
virtual void OfferAttach(std::weak_ptr<WrappedAttachInfo> attach_info) = 0;
|
||||
};
|
||||
|
||||
/// Trivial ProcessorController with debugging features disabled
|
||||
class DummyController final : public ProcessorController {
|
||||
void OnSegfault(uint32_t process_id, uint32_t thread_id) override {
|
||||
}
|
||||
|
||||
void OnBreakpoint(uint32_t process_id, uint32_t thread_id) override {
|
||||
}
|
||||
|
||||
void OnReadWatchpoint(uint32_t process_id, uint32_t thread_id, uint32_t data_address) override {
|
||||
}
|
||||
|
||||
void OnWriteWatchpoint(uint32_t process_id, uint32_t thread_id, uint32_t data_address) override {
|
||||
}
|
||||
|
||||
void OnProcessLaunched(uint32_t process_id, uint32_t thread_id, uint32_t launched_process_id) override {
|
||||
}
|
||||
|
||||
void OfferAttach(std::weak_ptr<WrappedAttachInfo> attach_info) override {
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Interpreter
|
||||
|
||||
namespace Interpreter {
|
||||
|
||||
struct Callsite {
|
||||
uint32_t source;
|
||||
uint32_t target;
|
||||
|
||||
ARM::State state;
|
||||
};
|
||||
|
||||
struct Setup;
|
||||
|
||||
struct CPUContext;
|
||||
|
||||
// #define CONTROL_FLOW_LOGGING
|
||||
struct ControlFlowLogger {
|
||||
uint32_t indent = 0;
|
||||
|
||||
void Branch(CPUContext& ctx, const char* kind, uint32_t addr);
|
||||
|
||||
void Return(CPUContext& ctx, const char* kind);
|
||||
|
||||
void SVC(CPUContext& ctx, uint32_t id);
|
||||
|
||||
void Log(CPUContext& ctx, const std::string& str);
|
||||
|
||||
private:
|
||||
std::string GetIndent();
|
||||
void MakeSureFileIsOpen(CPUContext& ctx);
|
||||
|
||||
#ifdef CONTROL_FLOW_LOGGING
|
||||
std::ofstream os;
|
||||
#endif
|
||||
};
|
||||
|
||||
// TODO: Move to private implementation
|
||||
struct CPUContext {
|
||||
CPUContext(HLE::OS::OS* os = nullptr, Setup* setup = nullptr);
|
||||
~CPUContext();
|
||||
|
||||
ARM::State cpu{};
|
||||
|
||||
std::list<Breakpoint> breakpoints;
|
||||
std::list<Breakpoint> read_watchpoints;
|
||||
std::list<Breakpoint> write_watchpoints;
|
||||
|
||||
std::vector<Callsite> backtrace;
|
||||
|
||||
// This must be true for any of the debugging functionality to be enabled
|
||||
bool debugger_attached = false;
|
||||
|
||||
bool trap_on_resume = false;
|
||||
|
||||
ControlFlowLogger cfl{};
|
||||
|
||||
void RecordCall(uint32_t source, uint32_t target, ARM::State state);
|
||||
|
||||
HLE::OS::OS* os{};
|
||||
|
||||
Setup* setup{};
|
||||
|
||||
ProcessorController* controller{};
|
||||
};
|
||||
|
||||
// TODO: This should be somewhere stored in its own header! (need to provide external default destructor in Processor to enable that
|
||||
// TODO: Rename to Emulator (and strip the CPUContext; they belong in the Processors' ExecutionContext instead, and those are created by OS)
|
||||
struct Setup {
|
||||
Setup(LogManager&, const KeyDatabase&,
|
||||
std::unique_ptr<Loader::GameCard>, Profiler::Profiler&,
|
||||
Debugger::DebugServer&);
|
||||
~Setup();
|
||||
|
||||
CPUContext cpus[2];
|
||||
|
||||
Memory::PhysicalMemory mem;
|
||||
|
||||
std::unique_ptr<HLE::OS::OS> os;
|
||||
|
||||
const KeyDatabase& keydb;
|
||||
|
||||
std::unique_ptr<Loader::GameCard> gamecard = nullptr;
|
||||
|
||||
Profiler::Profiler& profiler;
|
||||
|
||||
Debugger::DebugServer& debug_server;
|
||||
};
|
||||
|
||||
// TODO: Don't leak this implementation detail
|
||||
void Step(struct ExecutionContext&); // NOTE: This is only for the Interpreter
|
||||
|
||||
class Processor;
|
||||
|
||||
struct ExecutionContext {
|
||||
Processor& parent;
|
||||
|
||||
ExecutionContext(Processor& parent);
|
||||
|
||||
ExecutionContext(const ExecutionContext&) = delete;
|
||||
ExecutionContext& operator=(const ExecutionContext&) = delete;
|
||||
ExecutionContext(const ExecutionContext&&) = delete;
|
||||
ExecutionContext& operator=(ExecutionContext&&) = delete;
|
||||
|
||||
// Automatically unregisters this context from its parent Processor
|
||||
virtual ~ExecutionContext();
|
||||
|
||||
// TODO: Provide generic accessors instead
|
||||
virtual ARM::State ToGenericContext() = 0;
|
||||
virtual void FromGenericContext(const ARM::State&) = 0;
|
||||
|
||||
/**
|
||||
* Enables ProcessorController to take control over the thread corresponding to this context.
|
||||
*/
|
||||
virtual void SetDebuggingEnabled(bool enabled = true) { (void)enabled; }
|
||||
|
||||
virtual bool IsDebuggingEnabled() const { return false; }
|
||||
};
|
||||
|
||||
class Processor {
|
||||
friend struct ExecutionContext;
|
||||
|
||||
virtual ExecutionContext* CreateExecutionContextImpl() = 0;
|
||||
|
||||
void UnregisterContext(ExecutionContext&);
|
||||
|
||||
protected:
|
||||
std::vector<ExecutionContext*> contexts;
|
||||
|
||||
public:
|
||||
virtual ~Processor() = default;
|
||||
|
||||
virtual void WriteVirtualMemory8(uint32_t virt_address, const uint8_t value) = 0;
|
||||
virtual void WriteVirtualMemory16(uint32_t virt_address, const uint16_t value) = 0;
|
||||
virtual void WriteVirtualMemory32(uint32_t virt_address, const uint32_t value) = 0;
|
||||
|
||||
virtual uint8_t ReadVirtualMemory8(uint32_t virt_address) = 0;
|
||||
virtual uint16_t ReadVirtualMemory16(uint32_t virt_address) = 0;
|
||||
virtual uint32_t ReadVirtualMemory32(uint32_t virt_address) = 0;
|
||||
|
||||
// Runs until shut down by controller
|
||||
virtual void Run(ExecutionContext&, ProcessorController& controller, uint32_t process_id, uint32_t thread_id) = 0;
|
||||
|
||||
virtual void OnVirtualMemoryMapped(uint32_t phys_addr, uint32_t size, uint32_t vaddr) = 0;
|
||||
|
||||
virtual void OnVirtualMemoryUnmapped(uint32_t vaddr, uint32_t size) = 0;
|
||||
|
||||
std::unique_ptr<ExecutionContext> CreateExecutionContext() {
|
||||
return std::unique_ptr<ExecutionContext> { CreateExecutionContextImpl() };
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<Processor> CreateInterpreter(Setup& setup);
|
||||
|
||||
} // namespace Interpreter
|
154
source/ipc.cpp
Normal file
154
source/ipc.cpp
Normal file
|
@ -0,0 +1,154 @@
|
|||
#include "ipc.hpp"
|
||||
#include "os.hpp"
|
||||
|
||||
namespace HLE {
|
||||
|
||||
namespace IPC {
|
||||
|
||||
TLSReader::TLSReader(OS::Thread& thread) : thread(thread) {
|
||||
}
|
||||
|
||||
OS::Handle TLSReader::ParseHandle() {
|
||||
// TODO: Read the number of handles from the descriptor at "offset" rather
|
||||
// than assuming we should only read a single one!
|
||||
auto ret = thread.ReadTLS(offset);
|
||||
offset += 4;
|
||||
return {ret};
|
||||
}
|
||||
|
||||
OS::Result TLSReader::operator()(const IPC::CommandTags::result_tag& /* unused */) {
|
||||
uint32_t data = thread.ReadTLS(offset);
|
||||
offset += 4;
|
||||
return data;
|
||||
}
|
||||
|
||||
uint32_t TLSReader::operator()(const IPC::CommandTags::uint32_tag& /* unused */) {
|
||||
uint32_t data = thread.ReadTLS(offset);
|
||||
offset += 4;
|
||||
return data;
|
||||
}
|
||||
|
||||
uint64_t TLSReader::operator()(const IPC::CommandTags::uint64_tag& /* unused */) {
|
||||
uint64_t data = (static_cast<uint64_t>(thread.ReadTLS(offset + 4)) << 32) | thread.ReadTLS(offset);
|
||||
offset += 8;
|
||||
return data;
|
||||
}
|
||||
|
||||
IPC::StaticBuffer TLSReader::operator()(const IPC::CommandTags::static_buffer_tag& /* unused */) {
|
||||
// TLS@offset stores the translation descriptor, which is only relevant for the kernel
|
||||
IPC::TranslationDescriptor descriptor = { thread.ReadTLS(offset) };
|
||||
uint32_t buffer_addr = thread.ReadTLS(offset + 4);
|
||||
offset += 8;
|
||||
return { buffer_addr, descriptor.static_buffer.size.Value(), descriptor.static_buffer.id.Value() };
|
||||
}
|
||||
|
||||
IPC::StaticBuffer TLSReader::operator()(const IPC::CommandTags::pxi_buffer_tag<false>& /* unused */) {
|
||||
// TLS@offset stores the translation descriptor, which is only relevant for the kernel
|
||||
IPC::TranslationDescriptor descriptor = { thread.ReadTLS(offset) };
|
||||
uint32_t buffer_addr = thread.ReadTLS(offset + 4);
|
||||
offset += 8;
|
||||
// TODO: DONT DO THIS. Instead use the commented-out version.
|
||||
return { buffer_addr, descriptor.static_buffer.size.Value(), descriptor.static_buffer.id.Value() };
|
||||
// return { buffer_addr, descriptor.pxi_buffer.size.Value(), descriptor.pxi_buffer.id.Value() };
|
||||
}
|
||||
|
||||
IPC::StaticBuffer TLSReader::operator()(const IPC::CommandTags::pxi_buffer_tag<true>& /* unused */) {
|
||||
// TLS@offset stores the translation descriptor, which is only relevant for the kernel
|
||||
IPC::TranslationDescriptor descriptor = { thread.ReadTLS(offset) };
|
||||
uint32_t buffer_addr = thread.ReadTLS(offset + 4);
|
||||
offset += 8;
|
||||
// TODO: DONT DO THIS. Instead use the commented-out version.
|
||||
return { buffer_addr, descriptor.static_buffer.size.Value(), descriptor.static_buffer.id.Value() };
|
||||
// return { buffer_addr, descriptor.pxi_buffer.size.Value(), descriptor.pxi_buffer.id.Value() };
|
||||
}
|
||||
|
||||
OS::ProcessId TLSReader::operator()(const IPC::CommandTags::process_id_tag& /* unused */) {
|
||||
uint32_t process_id = thread.ReadTLS(offset + 4);
|
||||
offset += 8;
|
||||
return process_id;
|
||||
}
|
||||
|
||||
MappedBuffer TLSReader::operator()(const IPC::CommandTags::map_buffer_r_tag& /* unused */) {
|
||||
MappedBuffer ret;
|
||||
ret.size = TranslationDescriptor{thread.ReadTLS(offset)}.map_buffer.size;
|
||||
ret.addr = thread.ReadTLS(offset + 4);
|
||||
offset += 8;
|
||||
return ret;
|
||||
}
|
||||
|
||||
MappedBuffer TLSReader::operator()(const IPC::CommandTags::map_buffer_w_tag& /* unused */) {
|
||||
MappedBuffer ret;
|
||||
ret.size = TranslationDescriptor{thread.ReadTLS(offset)}.map_buffer.size;
|
||||
ret.addr = thread.ReadTLS(offset + 4);
|
||||
offset += 8;
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
TLSWriter::TLSWriter(OS::Thread& thread) : thread(thread) {
|
||||
}
|
||||
|
||||
void TLSWriter::WriteHandleDescriptor(const OS::Handle* data, size_t num, bool close) {
|
||||
thread.WriteTLS(offset, IPC::TranslationDescriptor::MakeHandles(num, close).raw);
|
||||
offset += 4;
|
||||
for (auto data_ptr = data; data_ptr != data + num; ++data_ptr) {
|
||||
thread.WriteTLS(offset, data_ptr->value);
|
||||
offset += 4;
|
||||
}
|
||||
}
|
||||
|
||||
void TLSWriter::operator()(IPC::CommandTags::result_tag, OS::Result data) {
|
||||
thread.WriteTLS(offset, data);
|
||||
offset += 4;
|
||||
}
|
||||
|
||||
void TLSWriter::operator()(IPC::CommandTags::uint32_tag, uint32_t data) {
|
||||
thread.WriteTLS(offset, data);
|
||||
offset += 4;
|
||||
}
|
||||
|
||||
void TLSWriter::operator()(IPC::CommandTags::uint64_tag, uint64_t data) {
|
||||
thread.WriteTLS(offset , data & 0xFFFFFFFF);
|
||||
thread.WriteTLS(offset + 4, data >> 32);
|
||||
offset += 8;
|
||||
}
|
||||
|
||||
void TLSWriter::operator()(IPC::CommandTags::map_buffer_r_tag, const MappedBuffer& buffer) {
|
||||
thread.WriteTLS(offset, IPC::TranslationDescriptor::MapBuffer(buffer.size, TranslationDescriptor::Read).raw);
|
||||
thread.WriteTLS(offset + 4, buffer.addr);
|
||||
offset += 8;
|
||||
}
|
||||
|
||||
void TLSWriter::operator()(IPC::CommandTags::map_buffer_w_tag, const MappedBuffer& buffer) {
|
||||
thread.WriteTLS(offset, IPC::TranslationDescriptor::MapBuffer(buffer.size, TranslationDescriptor::Write).raw);
|
||||
thread.WriteTLS(offset + 4, buffer.addr);
|
||||
offset += 8;
|
||||
}
|
||||
|
||||
void TLSWriter::operator()(IPC::CommandTags::static_buffer_tag, const StaticBuffer& buffer) {
|
||||
thread.WriteTLS(offset, IPC::TranslationDescriptor::MakeStaticBuffer(buffer.id, buffer.size).raw);
|
||||
thread.WriteTLS(offset + 4, buffer.addr);
|
||||
offset += 8;
|
||||
}
|
||||
|
||||
void TLSWriter::operator()(IPC::CommandTags::process_id_tag, const EmptyValue&) {
|
||||
thread.WriteTLS(offset, IPC::TranslationDescriptor::MakeProcessHandle().raw);
|
||||
thread.WriteTLS(offset + 4, 0);
|
||||
offset += 8;
|
||||
}
|
||||
|
||||
void TLSWriter::operator()(IPC::CommandTags::pxi_buffer_tag<false>, const StaticBuffer& buffer) {
|
||||
thread.WriteTLS(offset, IPC::TranslationDescriptor::MakePXIBuffer(buffer.id, buffer.size).raw);
|
||||
thread.WriteTLS(offset + 4, buffer.addr);
|
||||
offset += 8;
|
||||
}
|
||||
|
||||
void TLSWriter::operator()(IPC::CommandTags::pxi_buffer_tag<true>, const StaticBuffer& buffer) {
|
||||
thread.WriteTLS(offset, IPC::TranslationDescriptor::MakePXIConstBuffer(buffer.id, buffer.size).raw);
|
||||
thread.WriteTLS(offset + 4, buffer.addr);
|
||||
offset += 8;
|
||||
}
|
||||
|
||||
} // namespace IPC
|
||||
|
||||
} // namespace HLE
|
450
source/ipc.hpp
Normal file
450
source/ipc.hpp
Normal file
|
@ -0,0 +1,450 @@
|
|||
#pragma once
|
||||
|
||||
#include "framework/meta_tools.hpp"
|
||||
#include "platform/ipc.hpp"
|
||||
#include "os.hpp"
|
||||
|
||||
#include "bit_field.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace HLE {
|
||||
|
||||
namespace OS {
|
||||
class Thread;
|
||||
struct Handle;
|
||||
using Result = uint32_t;
|
||||
}
|
||||
|
||||
namespace IPC {
|
||||
|
||||
using namespace Platform::IPC;
|
||||
|
||||
/// Dummy structure used for IPC command tags that don't need any actual data
|
||||
struct EmptyValue {
|
||||
};
|
||||
|
||||
struct MappedBuffer {
|
||||
/*VAddr*/uint32_t addr;
|
||||
uint32_t size;
|
||||
|
||||
uint32_t EndAddr() const {
|
||||
return addr + size;
|
||||
}
|
||||
};
|
||||
|
||||
struct StaticBuffer {
|
||||
uint32_t addr;
|
||||
uint32_t size;
|
||||
uint32_t id;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
struct NormalParamToCommandTagHelper { using type = CommandTags::serialized_tag<T>; };
|
||||
template<>
|
||||
struct NormalParamToCommandTagHelper<uint32_t> { using type = CommandTags::uint32_tag; };
|
||||
template<>
|
||||
struct NormalParamToCommandTagHelper<uint64_t> { using type = CommandTags::uint64_tag; };
|
||||
|
||||
template<typename T>
|
||||
using NormalParamToCommandTag = typename NormalParamToCommandTagHelper<T>::type;
|
||||
|
||||
|
||||
template<typename F, typename T, typename ArgsTuple, size_t... Idxs>
|
||||
auto DoDeserialize(F& f, std::index_sequence<Idxs...>) {
|
||||
return Meta::CallWithSequentialEvaluation<decltype(detail::GetDeserializer<T>()(f(NormalParamToCommandTag<std::tuple_element_t<Idxs, ArgsTuple>>{})...))> {
|
||||
detail::GetDeserializer<T>(),
|
||||
f(NormalParamToCommandTag<std::tuple_element_t<Idxs, ArgsTuple>>{})...
|
||||
}.GetResult();
|
||||
}
|
||||
|
||||
/// Utility class to parse IPC messages from TLS
|
||||
class TLSReader {
|
||||
OS::Thread& thread;
|
||||
|
||||
// Skip header
|
||||
uint32_t offset = 0x84;
|
||||
|
||||
OS::Handle ParseHandle();
|
||||
|
||||
template<std::size_t Num>
|
||||
using HandleReturnType = std::conditional_t<Num == 1, OS::Handle, std::array<OS::Handle, Num>>;
|
||||
|
||||
public:
|
||||
TLSReader(OS::Thread& thread);
|
||||
TLSReader(const TLSReader&) = delete;
|
||||
TLSReader(TLSReader&&) = default;
|
||||
|
||||
OS::Result operator()(const IPC::CommandTags::result_tag& /* unused */);
|
||||
uint32_t operator()(const IPC::CommandTags::uint32_tag& /* unused */);
|
||||
uint64_t operator()(const IPC::CommandTags::uint64_tag& /* unused */);
|
||||
StaticBuffer operator()(const IPC::CommandTags::static_buffer_tag& /* unused */);
|
||||
StaticBuffer operator()(const IPC::CommandTags::pxi_buffer_tag<false>& /* unused */);
|
||||
StaticBuffer operator()(const IPC::CommandTags::pxi_buffer_tag<true>& /* unused */);
|
||||
/*ProcessId*/uint32_t operator()(const IPC::CommandTags::process_id_tag& /* unused */);
|
||||
MappedBuffer operator()(const IPC::CommandTags::map_buffer_r_tag& /* unused */);
|
||||
MappedBuffer operator()(const IPC::CommandTags::map_buffer_w_tag& /* unused */);
|
||||
|
||||
template<typename T>
|
||||
T operator()(const IPC::CommandTags::serialized_tag<T>& /* unused */) {
|
||||
using arg_list = typename Meta::function_traits<decltype(detail::GetDeserializer<T>())>::args;
|
||||
return DoDeserialize<TLSReader, T, arg_list>(*this, std::make_index_sequence<std::tuple_size<arg_list>::value>{});
|
||||
}
|
||||
|
||||
template<HandleType Type, std::size_t Num>
|
||||
HandleReturnType<Num> operator()(const IPC::CommandTags::handle_close_tag<Type, Num>& /* unused */) {
|
||||
return (*this)(IPC::CommandTags::handle_tag<Type, Num> { });
|
||||
}
|
||||
template<HandleType Type, std::size_t Num>
|
||||
HandleReturnType<Num> operator()(const IPC::CommandTags::handle_tag<Type, Num>& /* unused */) {
|
||||
offset += 4; // Move past descriptor
|
||||
if constexpr (Num == 1) {
|
||||
return ParseHandle();
|
||||
} else {
|
||||
HandleReturnType<Num> ret;
|
||||
for (auto& handle : ret) {
|
||||
handle = ParseHandle();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template<typename F, typename DataTuple, typename Data, size_t... Idxs>
|
||||
inline void DoWrite(F& f, const Data& data, std::index_sequence<Idxs...>) {
|
||||
(f(NormalParamToCommandTag<std::tuple_element_t<Idxs, DataTuple>>{}, std::get<Idxs>(data)), ...);
|
||||
}
|
||||
|
||||
/// Utility class to construct IPC messages
|
||||
class TLSWriter {
|
||||
OS::Thread& thread;
|
||||
|
||||
// Initial offset (response header omitted)
|
||||
uint32_t offset = 0x84;
|
||||
|
||||
void WriteHandleDescriptor(const OS::Handle* data, size_t num, bool close);
|
||||
|
||||
public:
|
||||
TLSWriter(OS::Thread& thread);
|
||||
TLSWriter(const TLSWriter& tls) = delete;
|
||||
TLSWriter(TLSWriter&& tls) = default;
|
||||
|
||||
void operator()(IPC::CommandTags::result_tag, OS::Result data);
|
||||
void operator()(IPC::CommandTags::uint32_tag, uint32_t data);
|
||||
void operator()(IPC::CommandTags::uint64_tag, uint64_t data);
|
||||
void operator()(IPC::CommandTags::map_buffer_r_tag, const MappedBuffer& buffer);
|
||||
void operator()(IPC::CommandTags::map_buffer_w_tag, const MappedBuffer& buffer);
|
||||
void operator()(IPC::CommandTags::static_buffer_tag, const StaticBuffer& buffer);
|
||||
void operator()(IPC::CommandTags::process_id_tag, const EmptyValue&);
|
||||
|
||||
void operator()(IPC::CommandTags::pxi_buffer_tag<false>, const StaticBuffer& buffer);
|
||||
void operator()(IPC::CommandTags::pxi_buffer_tag<true>, const StaticBuffer& buffer);
|
||||
|
||||
template<HandleType Type>
|
||||
void operator()(IPC::CommandTags::handle_close_tag<Type>, OS::Handle data) {
|
||||
WriteHandleDescriptor(&data, 1, true);
|
||||
}
|
||||
|
||||
template<HandleType Type>
|
||||
void operator()(IPC::CommandTags::handle_tag<Type>, OS::Handle data) {
|
||||
WriteHandleDescriptor(&data, 1, false);
|
||||
}
|
||||
|
||||
template<HandleType Type, size_t num>
|
||||
void operator()(IPC::CommandTags::handle_close_tag<Type,num>, const std::array<OS::Handle,num>& data) {
|
||||
WriteHandleDescriptor(data.data(), num, true);
|
||||
}
|
||||
|
||||
template<HandleType Type, size_t num>
|
||||
void operator()(IPC::CommandTags::handle_tag<Type,num>, const std::array<OS::Handle,num>& data) {
|
||||
WriteHandleDescriptor(data.data(), num, false);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void operator()(IPC::CommandTags::serialized_tag<T>, T& data) {
|
||||
auto parameters = detail::Serialize<T>(data);
|
||||
constexpr auto tuple_size = std::tuple_size<decltype(parameters)>::value;
|
||||
DoWrite<TLSWriter, decltype(parameters)>(*this, parameters, std::make_index_sequence<tuple_size>{});
|
||||
}
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
template<bool NoError>
|
||||
struct CHECK_EQUALITY_VERBOSELY_DEFAULT_ERROR { static_assert(NoError, "Given integers are not equal!"); };
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility structure for asserting that the compile-time constants A and B
|
||||
* are equal. If they aren't, the type Error<true> will be instantiated, which
|
||||
* is supposed to abort compilation due to a static_assert failure.
|
||||
*
|
||||
* The error message can be customized by passing an own Error template, which
|
||||
* is expected to have a static_assert fail if its instantiated with a "true"
|
||||
* argument. This is useful to give helpful error messages instead of cryptic
|
||||
* "5 != 3" messages.
|
||||
*
|
||||
* @todo Move this into a utility header
|
||||
*/
|
||||
template<int A, int B,
|
||||
template<bool Err> class Error = detail::CHECK_EQUALITY_VERBOSELY_DEFAULT_ERROR>
|
||||
struct CHECK_EQUALITY_VERBOSELY
|
||||
{
|
||||
// Force Error<A==B> to be instantiated and error out if A!=B
|
||||
// If A==B, use sizeof to form the value "true".
|
||||
static constexpr bool value = sizeof(Error<A==B>);
|
||||
};
|
||||
|
||||
/// Exception class thrown by SendIPCRequest on error
|
||||
struct IPCError {
|
||||
IPCError(uint32_t response_header, OS::Result result) noexcept : header(response_header), result(result) {
|
||||
}
|
||||
~IPCError() noexcept = default;
|
||||
|
||||
uint32_t header;
|
||||
OS::Result result;
|
||||
};
|
||||
|
||||
template<typename F, typename Tuple, size_t... Idxs>
|
||||
auto TransformTupleSequentiallyHelper(F&& f, Tuple&& tuple, std::index_sequence<Idxs...>)
|
||||
{
|
||||
return std::tuple<std::invoke_result_t<F, std::tuple_element_t<Idxs, Tuple>>...> { f(std::get<Idxs>(tuple))... };
|
||||
}
|
||||
|
||||
template<typename F, typename Tuple>
|
||||
auto TransformTupleSequentially(F&& f, Tuple&& tuple)
|
||||
{
|
||||
return TransformTupleSequentiallyHelper(std::forward<F>(f), std::forward<Tuple>(tuple), std::make_index_sequence<std::tuple_size<Tuple>::value>{});
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
template<bool NoError>
|
||||
struct IPCArgumentMatchError { static_assert(NoError, "Given number of function arguments doesn't match the expected number per the IPC message descriptor"); };
|
||||
|
||||
template<typename ResponseList>
|
||||
struct IPCResponseChecker;
|
||||
|
||||
template<typename... Parameters>
|
||||
struct IPCResponseChecker<boost::mp11::mp_list<Parameters...>> {
|
||||
void Check(Parameters...) {
|
||||
// Do nothing, just syntax-check this is a valid function call
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
struct TagToTypeHelper;
|
||||
template<>
|
||||
struct TagToTypeHelper<CommandTags::uint32_tag> : boost::mp11::mp_identity<uint32_t> {};
|
||||
template<>
|
||||
struct TagToTypeHelper<CommandTags::uint64_tag> : boost::mp11::mp_identity<uint64_t> {};
|
||||
template<>
|
||||
struct TagToTypeHelper<CommandTags::result_tag> : boost::mp11::mp_identity<OS::Result> {};
|
||||
template<HandleType T>
|
||||
struct TagToTypeHelper<CommandTags::handle_tag<T, 1>> : boost::mp11::mp_identity<OS::Handle> {};
|
||||
template<HandleType T>
|
||||
struct TagToTypeHelper<CommandTags::handle_close_tag<T, 1>> : boost::mp11::mp_identity<OS::Handle> {};
|
||||
template<HandleType T, std::size_t num>
|
||||
struct TagToTypeHelper<CommandTags::handle_tag<T, num>> : boost::mp11::mp_identity<std::array<OS::Handle, num>> {};
|
||||
template<HandleType T, std::size_t num>
|
||||
struct TagToTypeHelper<CommandTags::handle_close_tag<T, num>> : boost::mp11::mp_identity<std::array<OS::Handle, num>> {};
|
||||
template<>
|
||||
struct TagToTypeHelper<CommandTags::map_buffer_r_tag> : boost::mp11::mp_identity<MappedBuffer> {};
|
||||
template<>
|
||||
struct TagToTypeHelper<CommandTags::map_buffer_w_tag> : boost::mp11::mp_identity<MappedBuffer> {};
|
||||
template<>
|
||||
struct TagToTypeHelper<CommandTags::map_buffer_rw_tag> : boost::mp11::mp_identity<MappedBuffer> {};
|
||||
template<>
|
||||
struct TagToTypeHelper<CommandTags::static_buffer_tag> : boost::mp11::mp_identity<StaticBuffer> {};
|
||||
template<typename T>
|
||||
struct TagToTypeHelper<CommandTags::serialized_tag<T>> : boost::mp11::mp_identity<T> {};
|
||||
template<bool read_only>
|
||||
struct TagToTypeHelper<CommandTags::pxi_buffer_tag<read_only>> : boost::mp11::mp_identity<StaticBuffer> {};
|
||||
template<>
|
||||
struct TagToTypeHelper<CommandTags::process_id_tag> : boost::mp11::mp_identity<uint32_t> {};
|
||||
template<typename T>
|
||||
using TagToType = typename TagToTypeHelper<T>::type;
|
||||
|
||||
template<typename Command, bool IsResponse, typename Thread, typename ArgTagsTuple>
|
||||
struct IPCMessageWriter;
|
||||
|
||||
template<typename Command, bool IsResponse, typename Thread, typename... ArgTags>
|
||||
struct IPCMessageWriter< Command, IsResponse, Thread, boost::mp11::mp_list<ArgTags...>> {
|
||||
Thread& thread;
|
||||
|
||||
using TypeList = std::conditional_t<IsResponse, typename Command::response_list, typename Command::request_list>;
|
||||
|
||||
template<typename... Data>
|
||||
auto operator()(Data... data) {
|
||||
if constexpr (IsResponse) {
|
||||
// Check if the returned value matches the command definition
|
||||
// TODO: Enable this check for requests, too
|
||||
using paramlist = boost::mp11::mp_transform<TagToType, TypeList>;
|
||||
IPCResponseChecker<paramlist>{}.Check(data...);
|
||||
}
|
||||
|
||||
thread.WriteTLS(0x80, (IsResponse ? Command::response_header : Command::request_header));
|
||||
|
||||
IPC::TLSWriter writer(thread);
|
||||
(writer(ArgTags { }, data), ...);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
template<typename Command, typename Thread, typename... Data>
|
||||
void WriteIPCReplyFromTuple(Thread& thread, std::tuple<Data...> data) {
|
||||
std::apply(detail::IPCMessageWriter<Command, true, Thread, typename Command::response_list> { thread }, data);
|
||||
}
|
||||
|
||||
template<typename Func, typename MainArgs, typename... ExtraArgs>
|
||||
struct IPCMessageResultsHelper;
|
||||
|
||||
template<typename Func, typename... MainArgs, typename... ExtraArgs>
|
||||
struct IPCMessageResultsHelper<Func, boost::mp11::mp_list<MainArgs...>, ExtraArgs...>
|
||||
: std::invoke_result< Func,
|
||||
std::invoke_result_t<IPC::TLSReader, MainArgs>...,
|
||||
ExtraArgs...> {
|
||||
};
|
||||
|
||||
template<typename Func, typename Command, typename... ExtraArgs>
|
||||
using IPCMessageResults = IPCMessageResultsHelper<Func, typename Command::request_list, ExtraArgs...>;
|
||||
|
||||
template< typename Func, typename... ExtraArgs, typename... ArgTags>
|
||||
auto DispatchIPCMessageHelper(IPC::TLSReader reader, Func&& handler, ExtraArgs&&... args, boost::mp11::mp_list<ArgTags...>) {
|
||||
// Read request data from TLS (via ArgTags) and invoke request handler
|
||||
// TODO: The following may perform implicit conversion of uint64_t
|
||||
// parameters to uint32_t (or vice versa), hence it actually provides
|
||||
// less compile-time safety that we would like it to, currently.
|
||||
if constexpr (std::is_invocable_v< Func, ExtraArgs...,
|
||||
std::invoke_result_t<IPC::TLSReader, ArgTags>...>) {
|
||||
// Use CallWithSequentialEvaluation to get well-defined execution order
|
||||
// TODO: We should static assert for handlers returning void, but this somehow doesn't always compile currently
|
||||
// static_assert( !std::is_invocable_r_v<void, Func, ExtraArgs..., std::invoke_result_t<IPC::TLSReader, ArgTags>...>,
|
||||
// "Handler must have non-void return type");
|
||||
// std::move is needed to work around a clang bug with class template argument deduction
|
||||
return std::move(Meta::CallWithSequentialEvaluation {
|
||||
std::forward<Func>(handler),
|
||||
std::forward<ExtraArgs>(args)...,
|
||||
reader(ArgTags{})... }).GetResult();
|
||||
} else {
|
||||
// handler is not invocable with the given arguments:
|
||||
// Use a regular function call to get a good compiler diagnostic
|
||||
return std::forward<Func>(handler)(
|
||||
std::forward<ExtraArgs>(args)...,
|
||||
reader(ArgTags{})...);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read IPC request data from the thread's TLS and forward the decoded
|
||||
* arguments to the given handler.
|
||||
*/
|
||||
template<typename Command, typename Func, typename Thread, typename... ExtraArgs>
|
||||
auto DispatchIPCMessage(Func&& handler, Thread& thread, ExtraArgs&&... args) {
|
||||
if (thread.ReadTLS(0x80) != Command::request_header)
|
||||
throw std::runtime_error(fmt::format("Expected command header {:#x}, but got {:#x}", Command::request_header, thread.ReadTLS(0x80)));
|
||||
|
||||
return DispatchIPCMessageHelper<Func, ExtraArgs...>(IPC::TLSReader(thread), std::forward<Func>(handler), std::forward<ExtraArgs>(args)..., typename Command::request_list { });
|
||||
}
|
||||
|
||||
/**
|
||||
* Read IPC request data from the thread's TLS, forward the decoded
|
||||
* arguments to the given handler, and write results back to TLS
|
||||
*/
|
||||
template<typename Command, typename Func, typename Thread, typename... ExtraArgs>
|
||||
void HandleIPCCommand(Func&& handler, Thread& thread, ExtraArgs&&... args) {
|
||||
// Decode request data from TLS and invoke handler
|
||||
auto output_data = DispatchIPCMessage<Command>(std::forward<Func>(handler), thread, std::forward<ExtraArgs>(args)...);
|
||||
|
||||
// Write response to TLS
|
||||
// TODO: Do not pass output_data per copy!
|
||||
WriteIPCReplyFromTuple<Command>(thread, output_data);
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
/**
|
||||
* Unwrap small tuples:
|
||||
* For single-element tuples, this returns the contained element.
|
||||
* For zero-element tuples, this returns void.
|
||||
* Otherwise, this just returns the unmodified tuple.
|
||||
*/
|
||||
template<typename... T>
|
||||
inline auto UnwrapTuple(std::tuple<T...>&& tuple) {
|
||||
return tuple;
|
||||
}
|
||||
|
||||
template<typename... T>
|
||||
inline decltype(auto) UnwrapTuple(std::tuple<T...>& tuple) {
|
||||
return tuple;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
inline decltype(auto) UnwrapTuple(std::tuple<T>&& tuple) {
|
||||
return std::get<0>(tuple);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
inline decltype(auto) UnwrapTuple(std::tuple<T>& tuple) {
|
||||
return std::get<0>(tuple);
|
||||
}
|
||||
|
||||
inline void UnwrapTuple(std::tuple<>&&) {
|
||||
}
|
||||
|
||||
inline void UnwrapTuple(std::tuple<>&) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit an IPC request from the given thread through the given session
|
||||
* handle. The request parameters are taken from the parameter pack, which is
|
||||
* expected to be compatible with the parameter list inferred from the Command
|
||||
* type.
|
||||
* @return A tuple of return values inferred from the given Command. This does
|
||||
* not include the return code. If only a single value is returned, the
|
||||
* tuple is unwrapped to the value itself. If no value is returned,
|
||||
* this function returns void.
|
||||
* @throws IPCError if sending the IPC request was unsuccessful, if the
|
||||
* response indicates a failure, and if the response header
|
||||
* is different from the one indicated by Command.
|
||||
* @todo Consider changing "t..." to use (universal) reference semantics
|
||||
* @todo Currently, things like the process_id tag require an EmptyValue
|
||||
* dummy argument, despite not actually needing any input data
|
||||
*/
|
||||
template<typename Command, typename Thread, typename... T>
|
||||
auto SendIPCRequest(Thread& thread, OS::Handle session_handle, T... t) {
|
||||
// Write request data to TLS
|
||||
detail::IPCMessageWriter<Command, false, Thread, typename Command::request_list> { thread } (t...);
|
||||
|
||||
// Send IPC request and wait for response
|
||||
// TODO: The command header returned here shouldn't be read from TLS but
|
||||
// rather constructed on-the-fly!
|
||||
OS::Result result = std::get<0>(thread.CallSVC(&OS::OS::SVCSendSyncRequest, session_handle));
|
||||
if (result != 0 /* RESULT_OK */)
|
||||
throw IPCError(thread.ReadTLS(0x080), result);
|
||||
|
||||
// Read result code of the response
|
||||
auto reader = TLSReader(thread);
|
||||
result = reader(CommandTags::result_tag{});
|
||||
if (result != 0)
|
||||
throw IPCError{thread.ReadTLS(0x80), result};
|
||||
|
||||
// TODO: Currently, we build the response header incorrectly and hence
|
||||
// cannot actually perform this important check
|
||||
//assert(thread.ReadTLS(0x80) == Command::response_header);
|
||||
|
||||
// Read results, but before that drop the result code (if it doesn't
|
||||
// indicate success, we aborted before anyway)
|
||||
using ResponseTypeList = boost::mp11::mp_rename<typename Command::response_list, std::tuple>;
|
||||
// TODO: Filter out EmptyValue values!
|
||||
using FixedResponseTypeList = boost::mp11::mp_pop_front<ResponseTypeList>;
|
||||
auto result_data = TransformTupleSequentially(reader, FixedResponseTypeList{});
|
||||
|
||||
// Return data: Decay to "void" or an unwrapped type if the data tuple is
|
||||
// empty or a singleton, respectively
|
||||
return detail::UnwrapTuple(result_data);
|
||||
}
|
||||
|
||||
} // namespace IPC
|
||||
|
||||
} // namespace HLE
|
23
source/loader/firm.cpp
Normal file
23
source/loader/firm.cpp
Normal file
|
@ -0,0 +1,23 @@
|
|||
#include "firm.hpp"
|
||||
|
||||
namespace Loader {
|
||||
|
||||
bool IsFirm(std::istream& str) {
|
||||
auto file_begin = str.tellg();
|
||||
|
||||
unsigned char magic[4];
|
||||
str.read(reinterpret_cast<char*>(magic), sizeof(magic));
|
||||
str.seekg(file_begin);
|
||||
|
||||
if (str.gcount() != sizeof(magic) ||
|
||||
magic[0] != 'F' ||
|
||||
magic[1] != 'I' ||
|
||||
magic[2] != 'R' ||
|
||||
magic[3] != 'M') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Loader
|
45
source/loader/firm.hpp
Normal file
45
source/loader/firm.hpp
Normal file
|
@ -0,0 +1,45 @@
|
|||
#pragma once
|
||||
|
||||
#include <boost/endian/arithmetic.hpp>
|
||||
|
||||
#include <istream>
|
||||
|
||||
namespace FIRM {
|
||||
|
||||
using boost::endian::little_uint32_t;
|
||||
|
||||
struct SectionHeader {
|
||||
little_uint32_t file_offset; // offset within file to section data
|
||||
little_uint32_t load_address; // physical memory address to load section data to
|
||||
little_uint32_t size; // size in bytes (may be 0)
|
||||
little_uint32_t type; // 0 = ARM9, 1 = ARM11
|
||||
uint8_t hash[0x20]; // SHA-256 hash of the section data
|
||||
};
|
||||
static_assert(sizeof(SectionHeader) == 0x30, "Incorrect FIRM section header size");
|
||||
|
||||
struct Header {
|
||||
char magic[4]; // "FIRM"
|
||||
|
||||
char reserved1[4];
|
||||
|
||||
little_uint32_t entry_arm11; // physical memory address
|
||||
little_uint32_t entry_arm9; // physical memory address
|
||||
|
||||
char reserved2[0x30];
|
||||
|
||||
SectionHeader sections[4];
|
||||
|
||||
uint8_t signature[0x100]; // RSA-2048 signature of the header
|
||||
};
|
||||
static_assert(sizeof(Header) == 0x200, "Incorrect FIRM header size");
|
||||
|
||||
} // namespace FIRM
|
||||
|
||||
namespace Loader {
|
||||
|
||||
/**
|
||||
* @note The stream read cursor is restored when this function returns
|
||||
*/
|
||||
bool IsFirm(std::istream& str);
|
||||
|
||||
} // namespace Loader
|
561
source/loader/gamecard.cpp
Normal file
561
source/loader/gamecard.cpp
Normal file
|
@ -0,0 +1,561 @@
|
|||
#include "gamecard.hpp"
|
||||
#include "platform/file_formats/ncch.hpp"
|
||||
#include "platform/file_formats/3dsx.hpp"
|
||||
#include "processes/pxi_fs.hpp"
|
||||
#include "processes/pxi.hpp"
|
||||
#include "host_file.hpp"
|
||||
|
||||
#include <framework/exceptions.hpp>
|
||||
|
||||
#include <range/v3/algorithm/copy.hpp>
|
||||
#include <range/v3/algorithm/equal.hpp>
|
||||
#include <range/v3/algorithm/fill.hpp>
|
||||
#include <range/v3/algorithm/transform.hpp>
|
||||
|
||||
#include <spdlog/sinks/null_sink.h>
|
||||
|
||||
namespace Loader {
|
||||
|
||||
GameCard::~GameCard() = default;
|
||||
|
||||
GameCardFromCXI::GameCardFromCXI(std::string_view filename) : source(std::string { filename }) {
|
||||
}
|
||||
|
||||
GameCardFromCXI::GameCardFromCXI(int file_descriptor) : source(file_descriptor) {
|
||||
}
|
||||
|
||||
struct GameCardSourceOpener {
|
||||
std::unique_ptr<HLE::PXI::FS::File>
|
||||
operator()(const std::string& filename) const {
|
||||
return std::unique_ptr<HLE::PXI::FS::File>(new HLE::PXI::FS::HostFile(filename, HLE::PXI::FS::HostFile::Default));
|
||||
}
|
||||
|
||||
std::unique_ptr<HLE::PXI::FS::File>
|
||||
operator()(int file_descriptor) const {
|
||||
return FileSystem::OpenNativeFile(file_descriptor);
|
||||
}
|
||||
};
|
||||
|
||||
std::optional<std::unique_ptr<HLE::PXI::FS::File>> GameCardFromCXI::GetPartitionFromId(NCSDPartitionId id) {
|
||||
if (id == NCSDPartitionId::Executable) {
|
||||
return std::visit(GameCardSourceOpener{}, source);
|
||||
} else {
|
||||
// TODO: Provide dummy-partitions instead
|
||||
throw std::runtime_error("Attempted to open non-existing partition from CXI game image");
|
||||
}
|
||||
}
|
||||
|
||||
static bool IsLoadableCXIFile(HLE::PXI::FS::File& file) {
|
||||
// TODO: Enable proper logging
|
||||
auto logger = std::make_shared<spdlog::logger>("dummy", std::make_shared<spdlog::sinks::null_sink_st>());
|
||||
auto file_context = HLE::PXI::FS::FileContext { *logger };
|
||||
file.Open(file_context, false);
|
||||
|
||||
FileFormat::NCCHHeader header;
|
||||
auto [result, bytes_read] = file.Read(file_context, 0, sizeof(header), HLE::PXI::FS::FileBufferInHostMemory(header));
|
||||
if (result != HLE::OS::RESULT_OK || bytes_read != sizeof(header)) {
|
||||
return false;
|
||||
}
|
||||
return ranges::equal(header.magic, std::string_view { "NCCH" });
|
||||
}
|
||||
|
||||
bool GameCardFromCXI::IsLoadableFile(std::string_view filename) {
|
||||
auto file = HLE::PXI::FS::HostFile(filename, {});
|
||||
return IsLoadableCXIFile(file);
|
||||
}
|
||||
|
||||
bool GameCardFromCXI::IsLoadableFile(int file_descriptor) {
|
||||
return IsLoadableCXIFile(*FileSystem::OpenNativeFile(file_descriptor));
|
||||
}
|
||||
|
||||
GameCardFromCCI::GameCardFromCCI(std::string_view filename) : source(std::string { filename }) {
|
||||
}
|
||||
|
||||
GameCardFromCCI::GameCardFromCCI(int file_descriptor) : source(file_descriptor) {
|
||||
}
|
||||
|
||||
std::optional<std::unique_ptr<HLE::PXI::FS::File>> GameCardFromCCI::GetPartitionFromId(NCSDPartitionId id) {
|
||||
// TODO: Enable logging
|
||||
auto logger = std::make_shared<spdlog::logger>("dummy", std::make_shared<spdlog::sinks::null_sink_st>());
|
||||
auto file_context = HLE::PXI::FS::FileContext { *logger };
|
||||
auto cci_file = std::visit(GameCardSourceOpener{}, source);
|
||||
|
||||
cci_file->Open(file_context, false);
|
||||
FileFormat::NCSDHeader ncsd;
|
||||
cci_file->Read(file_context, 0, decltype(ncsd)::Tags::expected_serialized_size, HLE::PXI::FS::FileBufferInHostMemory(ncsd));
|
||||
cci_file->Close();
|
||||
|
||||
auto& ncsd_partition = ncsd.partition_table[Meta::to_underlying(id)];
|
||||
auto ncch_file = std::unique_ptr<HLE::PXI::FS::File>(new HLE::PXI::FS::FileView(std::move(cci_file), ncsd_partition.offset.ToBytes(), ncsd_partition.size.ToBytes()));
|
||||
return std::move(ncch_file);
|
||||
}
|
||||
|
||||
static bool IsLoadableCCIFile(HLE::PXI::FS::File& file) {
|
||||
// TODO: Enable proper logging
|
||||
auto logger = std::make_shared<spdlog::logger>("dummy", std::make_shared<spdlog::sinks::null_sink_st>());
|
||||
auto file_context = HLE::PXI::FS::FileContext { *logger };
|
||||
file.Open(file_context, false);
|
||||
|
||||
FileFormat::NCSDHeader header;
|
||||
auto [result, bytes_read] = file.Read(file_context, 0, sizeof(header), HLE::PXI::FS::FileBufferInHostMemory(header));
|
||||
if (result != HLE::OS::RESULT_OK || bytes_read != sizeof(header)) {
|
||||
return false;
|
||||
}
|
||||
return ranges::equal(header.magic, std::string_view { "NCSD" });
|
||||
}
|
||||
|
||||
bool GameCardFromCCI::IsLoadableFile(std::string_view filename) {
|
||||
auto file = HLE::PXI::FS::HostFile(filename, {});
|
||||
return IsLoadableCCIFile(file);
|
||||
}
|
||||
|
||||
bool GameCardFromCCI::IsLoadableFile(int file_descriptor) {
|
||||
return IsLoadableCCIFile(*FileSystem::OpenNativeFile(file_descriptor));
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* Adaptor to make 3DSX files "look" like NCCHs.
|
||||
*/
|
||||
class Adaptor3DSXToNCCH : public HLE::PXI::FS::File {
|
||||
std::unique_ptr<HLE::PXI::FS::File> file; // 3DSX source data
|
||||
|
||||
// TODO: Actually store the memory in here!
|
||||
|
||||
FileFormat::NCCHHeader ncch = {};
|
||||
FileFormat::ExHeader exheader = {};
|
||||
|
||||
FileFormat::Dot3DSX::Header dot3dsxheader = {};
|
||||
|
||||
public:
|
||||
Adaptor3DSXToNCCH(std::unique_ptr<HLE::PXI::FS::File> file_3dsx) : file(std::move(file_3dsx)) {
|
||||
// Create a fake ncch
|
||||
ncch = FileFormat::NCCHHeader{};
|
||||
ncch.magic = std::array<uint8_t, 4>{{'N', 'C', 'C', 'H'}};
|
||||
// TODO: content_size
|
||||
ncch.partition_id = 0xdeadbeefdeadbeef;
|
||||
ncch.program_id = 0xdeadbeefdeadbeef;
|
||||
ncch.crypto_method = 0;
|
||||
ncch.platform = 1; // Old3DS
|
||||
ncch.type_mask = 0; // 3 ???
|
||||
ncch.unit_size_log2 = 0;
|
||||
ncch.flags = 0x1 | 0x2 | 0x4; // don't mount RomFS; don't encrypt contents
|
||||
ncch.exefs_hash_message_size = FileFormat::MediaUnit32{1};
|
||||
ncch.romfs_hash_message_size = FileFormat::MediaUnit32{1};
|
||||
|
||||
ncch.exheader_size = 0x400;
|
||||
|
||||
|
||||
// Generate fake exheader with maximum permissions
|
||||
// TODO: Restrict to fields which are actually necessary in emulation
|
||||
// TODO: Parts of this are re-initialized in Read()!
|
||||
exheader = {};
|
||||
exheader.application_title = {};
|
||||
exheader.unknown = {};
|
||||
exheader.flags = {};//exheader.flags.compress_exefs_code().Set(0);
|
||||
exheader.remaster_version = {};
|
||||
// Filled out by Generate3DSX
|
||||
// exheader.section_text = FileFormat::ExHeader::CodeSetInfo{text_vaddr, text_numpages, header.text_size};
|
||||
// exheader.section_ro = FileFormat::ExHeader::CodeSetInfo{ro_vaddr, ro_numpages, header.ro_size};
|
||||
// exheader.section_data = FileFormat::ExHeader::CodeSetInfo{data_vaddr, data_numpages, header.data_bss_size - header.bss_size};
|
||||
// exheader.bss_size = header.bss_size;
|
||||
exheader.stack_size = 0x4000; // TODO: Should we adjust this to some other value?
|
||||
exheader.stack_size = 0x20000; // TODO: Should we adjust this to some other value?
|
||||
exheader.unknown3 = {};
|
||||
exheader.unknown4 = {};
|
||||
|
||||
exheader.jump_id = {}; // TODO: Consider passing in alt_titleid here
|
||||
exheader.save_data_size = {};
|
||||
exheader.dependencies = {};
|
||||
// Use a set of default dependencies to ensure the system is in a reasonable state when we launch this 3dsx on boot
|
||||
std::vector<uint64_t> exheader_dependencies = {
|
||||
uint64_t { 0x0004013000008002 /* ns */ },
|
||||
uint64_t { 0x0004013000001c02 /* gsp */ },
|
||||
uint64_t { 0x0004013000001d02 /* hid */ },
|
||||
uint64_t { 0x0004013000001802 /* codec */ },
|
||||
uint64_t { 0x0004013000003102 /* ps */ },
|
||||
uint64_t { 0x0004013000002202 /* ptm */ },
|
||||
};
|
||||
ranges::copy(exheader_dependencies, exheader.dependencies.begin());
|
||||
|
||||
exheader.subsignature = {};
|
||||
exheader.ncch_sig_pub_key = {};
|
||||
|
||||
|
||||
// exheader.aci.program_id = alt_titleid;
|
||||
exheader.aci.version = 2;
|
||||
exheader.aci.flags = FileFormat::ExHeader::ACIFlags{}.flag0()(4).flag1()(3).flag2()(2).priority()(0x30);
|
||||
exheader.aci.unknown5 = {};
|
||||
|
||||
ranges::fill(exheader.aci.arm11_kernel_capabilities, FileFormat::ExHeader::ARM11KernelCapabilityDescriptor{0xffffffff});
|
||||
using SVCMaskTableEntry = FileFormat::ExHeader::ARM11KernelCapabilityDescriptor::SVCMaskTableEntry;
|
||||
using HandleTableInfo = FileFormat::ExHeader::ARM11KernelCapabilityDescriptor::HandleTableInfo;
|
||||
using MappedMemoryRange = FileFormat::ExHeader::ARM11KernelCapabilityDescriptor::MappedMemoryRange;
|
||||
using KernelFlags = FileFormat::ExHeader::ARM11KernelCapabilityDescriptor::KernelFlags;
|
||||
|
||||
exheader.aci.arm11_kernel_capabilities[0].storage = SVCMaskTableEntry::Make().entry_index()(0).mask()(0xfffffe).storage;
|
||||
exheader.aci.arm11_kernel_capabilities[1].storage = SVCMaskTableEntry::Make().entry_index()(1).mask()(0xffffff).storage;
|
||||
exheader.aci.arm11_kernel_capabilities[2].storage = SVCMaskTableEntry::Make().entry_index()(2).mask()(0x807fff).storage;
|
||||
exheader.aci.arm11_kernel_capabilities[3].storage = SVCMaskTableEntry::Make().entry_index()(3).mask()(0x1ffff).storage;
|
||||
exheader.aci.arm11_kernel_capabilities[4].storage = SVCMaskTableEntry::Make().entry_index()(4).mask()(0xef3fff).storage;
|
||||
exheader.aci.arm11_kernel_capabilities[5].storage = SVCMaskTableEntry::Make().entry_index()(5).mask()(0x3f).storage;
|
||||
exheader.aci.arm11_kernel_capabilities[6].storage = HandleTableInfo::Make().num_handles()(0x200).storage; // TODO: 0x200 handles is somewhat overkill...
|
||||
exheader.aci.arm11_kernel_capabilities[7].storage = MappedMemoryRange::Make().page_index()(0x1ff00).storage;
|
||||
exheader.aci.arm11_kernel_capabilities[8].storage = MappedMemoryRange::Make().page_index()(0x1ff80).storage;
|
||||
exheader.aci.arm11_kernel_capabilities[9].storage = MappedMemoryRange::Make().page_index()(0x1f000).read_only()(1).storage;
|
||||
exheader.aci.arm11_kernel_capabilities[10].storage = MappedMemoryRange::Make().page_index()(0x1f600).read_only()(1).storage;
|
||||
exheader.aci.arm11_kernel_capabilities[11].storage = KernelFlags::Make().memory_type()(1).storage;
|
||||
|
||||
// Map all IO memory
|
||||
exheader.aci.arm11_kernel_capabilities[12].storage = MappedMemoryRange::Make().page_index()(0x1ec00).storage;
|
||||
exheader.aci.arm11_kernel_capabilities[13].storage = MappedMemoryRange::Make().page_index()(0x1f000).storage;
|
||||
|
||||
exheader.aci.service_access_list = {};
|
||||
const char* default_services[] = {
|
||||
"APT:U", "ac:u", "am:net", "boss:U", "cam:u",
|
||||
"cecd:u", "cfg:nor", "cfg:u", "csnd:SND",
|
||||
"dsp::DSP", "frd:u", "fs:USER", "gsp::Gpu",
|
||||
"gsp::Lcd", "hid:USER", "http:C", "ir:rst",
|
||||
"ir:u", "ir:USER", "mic:u", "ndm:u",
|
||||
"news:s", "nwm::EXT", "nwm::UDS", "ptm:sysm",
|
||||
"ptm:u", "pxi:dev", "soc:U", "ssl:C",
|
||||
"y2r:u", "pm:app"
|
||||
};
|
||||
// static_assert(sizeof(default_services) / sizeof(default_services[0]) < exheader.aci.service_access_list.size(), // TODO: Not a constant expression?
|
||||
// "Maximum number of services exhausted");
|
||||
ranges::transform(default_services, exheader.aci.service_access_list.begin(),
|
||||
[](auto& service) {
|
||||
std::array<uint8_t, 8> ret{};
|
||||
ranges::copy(service, service + strlen(service), ret.begin());
|
||||
return ret;
|
||||
});
|
||||
|
||||
exheader.aci.unknown6 = {};
|
||||
exheader.aci.unknown7 = {};
|
||||
// TODO: Not sure what these are about.
|
||||
exheader.aci.unknown7[0x10] = 0xff;
|
||||
exheader.aci.unknown7[0x11] = 0x3;
|
||||
exheader.aci.unknown7[0x1f] = 0x2;
|
||||
exheader.aci_limits = exheader.aci;
|
||||
|
||||
// Ideal processor in primary ACI must be smaller or equal to the one in the secondary (TODO: Verify!)
|
||||
// Priority in primary ACI must be larger or equal to the one in the secondary (TODO: Verify!)
|
||||
exheader.aci_limits.flags = exheader.aci_limits.flags.ideal_processor()(1).priority()(0x18);
|
||||
}
|
||||
|
||||
HLE::OS::OS::ResultAnd<> Open(HLE::PXI::FS::FileContext& context, bool create_or_truncate) override {
|
||||
if (create_or_truncate) {
|
||||
throw std::runtime_error("Can't create/truncate game cards");
|
||||
}
|
||||
|
||||
auto ret = file->Open(context, create_or_truncate);
|
||||
if (ret != std::tie(HLE::OS::RESULT_OK)) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
FileFormat::Dot3DSX::Header Read3DSXHeader(HLE::PXI::FS::FileContext& context) {
|
||||
// TODO: Use serialization interface instead
|
||||
FileFormat::Dot3DSX::Header dot3dsxheader;
|
||||
auto ret = file->Read(context, 0, sizeof(dot3dsxheader), HLE::PXI::FS::FileBufferInHostMemory(dot3dsxheader));
|
||||
if (ret != HLE::OS::OS::ResultAnd<uint32_t> { HLE::OS::RESULT_OK, sizeof(dot3dsxheader) }) {
|
||||
throw std::runtime_error("Failed to read 3DSX header");
|
||||
}
|
||||
|
||||
// TODO: Check magic, check header size
|
||||
|
||||
return dot3dsxheader;
|
||||
}
|
||||
|
||||
std::optional<FileFormat::Dot3DSX::SecondaryHeader> Read3DSXSecondaryHeader(HLE::PXI::FS::FileContext& context, uint32_t header_size) {
|
||||
if (header_size == sizeof(FileFormat::Dot3DSX::Header)) {
|
||||
// No secondary header present
|
||||
return std::nullopt;
|
||||
} else if (header_size != sizeof(FileFormat::Dot3DSX::Header) + sizeof(FileFormat::Dot3DSX::SecondaryHeader)) {
|
||||
throw std::runtime_error("Unexpected header size in 3DSX file");
|
||||
}
|
||||
|
||||
// TODO: Use serialization interface instead
|
||||
FileFormat::Dot3DSX::SecondaryHeader secondary_header;
|
||||
auto ret = file->Read(context, sizeof(FileFormat::Dot3DSX::Header), sizeof(secondary_header), HLE::PXI::FS::FileBufferInHostMemory(secondary_header));
|
||||
if (ret != HLE::OS::OS::ResultAnd<uint32_t> { HLE::OS::RESULT_OK, sizeof(secondary_header) }) {
|
||||
throw std::runtime_error("Failed to read 3DSX header");
|
||||
}
|
||||
|
||||
return secondary_header;
|
||||
}
|
||||
|
||||
/// Get program code size in bytes
|
||||
uint64_t GetProgramCodeSize(const FileFormat::Dot3DSX::Header& dot3dsxheader) const {
|
||||
uint32_t text_numpages = ((dot3dsxheader.text_size + 0xfff) >> 12);
|
||||
uint32_t ro_numpages = ((dot3dsxheader.ro_size + 0xfff) >> 12);
|
||||
uint32_t data_numpages = (((dot3dsxheader.data_bss_size - dot3dsxheader.bss_size) + 0xfff) >> 12);
|
||||
|
||||
return (text_numpages + ro_numpages + data_numpages) << 12;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> ReadProgramCode(HLE::PXI::FS::FileContext& context, const FileFormat::Dot3DSX::Header& dot3dsxheader) {
|
||||
std::array<FileFormat::Dot3DSX::RelocationHeader, 3> relocation_headers;
|
||||
|
||||
std::vector<FileFormat::Dot3DSX::RelocationInfo> relocations;
|
||||
uint64_t offset = dot3dsxheader.header_size;
|
||||
for (size_t header = 0; header < relocation_headers.size(); ++header) {
|
||||
auto& reloc_header = relocation_headers[header];
|
||||
auto ret = file->Read(context, offset, sizeof(reloc_header), HLE::PXI::FS::FileBufferInHostMemory(reloc_header));
|
||||
if (std::get<0>(ret) != HLE::OS::RESULT_OK) {
|
||||
throw std::runtime_error("Failed to read ExeFS data");
|
||||
}
|
||||
offset += std::get<1>(ret);
|
||||
}
|
||||
|
||||
|
||||
// Read program sections
|
||||
uint32_t text_numpages = ((dot3dsxheader.text_size + 0xfff) >> 12);
|
||||
uint32_t ro_numpages = ((dot3dsxheader.ro_size + 0xfff) >> 12);
|
||||
uint32_t data_numpages = (((dot3dsxheader.data_bss_size - dot3dsxheader.bss_size) + 0xfff) >> 12);
|
||||
|
||||
uint32_t text_vaddr = 0x00100000;
|
||||
uint32_t ro_vaddr = text_vaddr + (text_numpages << 12);
|
||||
uint32_t data_vaddr = ro_vaddr + (ro_numpages << 12);
|
||||
|
||||
uint32_t total_size = GetProgramCodeSize(dot3dsxheader);
|
||||
std::vector<uint32_t> program_data(total_size / 4, 0);
|
||||
const std::array<uint32_t*, 3> segment_ptrs = {{ program_data.data(),
|
||||
program_data.data() + (ro_vaddr - text_vaddr) / 4,
|
||||
program_data.data() + (data_vaddr - text_vaddr) / 4 }};
|
||||
|
||||
// TODO: Endianness!
|
||||
{
|
||||
auto ret = file->Read(context, offset, dot3dsxheader.text_size,
|
||||
HLE::PXI::FS::FileBufferInHostMemory(segment_ptrs[0], dot3dsxheader.text_size));
|
||||
if (std::get<0>(ret) != HLE::OS::RESULT_OK) {
|
||||
throw std::runtime_error("Failed to read ExeFS data");
|
||||
}
|
||||
offset += std::get<1>(ret);
|
||||
|
||||
ret = file->Read(context, offset, dot3dsxheader.ro_size,
|
||||
HLE::PXI::FS::FileBufferInHostMemory(segment_ptrs[1], dot3dsxheader.ro_size));
|
||||
if (std::get<0>(ret) != HLE::OS::RESULT_OK) {
|
||||
throw std::runtime_error("Failed to read ExeFS data");
|
||||
}
|
||||
offset += std::get<1>(ret);
|
||||
|
||||
ret = file->Read(context, offset, dot3dsxheader.data_bss_size - dot3dsxheader.bss_size,
|
||||
HLE::PXI::FS::FileBufferInHostMemory(segment_ptrs[2], dot3dsxheader.data_bss_size - dot3dsxheader.bss_size));
|
||||
if (std::get<0>(ret) != HLE::OS::RESULT_OK) {
|
||||
throw std::runtime_error("Failed to read ExeFS data");
|
||||
}
|
||||
offset += std::get<1>(ret);
|
||||
}
|
||||
|
||||
// Patch in relocations
|
||||
for (unsigned segment = 0; segment < 3; ++segment) {
|
||||
auto& reloc_header = relocation_headers[segment];
|
||||
|
||||
// patch_kind = 0: absolute relocation; patch_kind = 1: relative relocation
|
||||
for (unsigned patch_kind = 0; patch_kind < 2; ++patch_kind) {
|
||||
unsigned num_relocations = (patch_kind==0) ? reloc_header.num_abs_relocs : reloc_header.num_rel_relocs;
|
||||
|
||||
// Pointer to the word currently being patched
|
||||
uint32_t* patch_ptr = segment_ptrs[segment];
|
||||
|
||||
for (unsigned relocation = 0; relocation < num_relocations; ++relocation) {
|
||||
FileFormat::Dot3DSX::RelocationInfo reloc_info;
|
||||
auto ret = file->Read(context, offset, sizeof(reloc_info),
|
||||
HLE::PXI::FS::FileBufferInHostMemory(reloc_info));
|
||||
if (std::get<0>(ret) != HLE::OS::RESULT_OK) {
|
||||
throw std::runtime_error("Failed to read ExeFS data");
|
||||
}
|
||||
offset += std::get<1>(ret);
|
||||
|
||||
patch_ptr += reloc_info.words_to_skip;
|
||||
for (unsigned word_index = 0; word_index < reloc_info.words_to_patch; ++word_index) {
|
||||
// TODO: Make sure patch_ptr is still within the current segment!
|
||||
|
||||
uint32_t unpatched_word = *patch_ptr;
|
||||
uint32_t virtual_address = std::invoke([=]() {
|
||||
// Translate offset given by unpatched_word into the target virtual address
|
||||
auto offset = (unpatched_word & 0x0FFFFFFF);
|
||||
auto numbytes_text = (text_numpages << 12);
|
||||
auto numbytes_text_and_ro = (text_numpages << 12) + (ro_numpages << 12);
|
||||
if (offset < numbytes_text) {
|
||||
return offset + text_vaddr;
|
||||
} else if (offset < numbytes_text_and_ro) {
|
||||
return offset + ro_vaddr - numbytes_text;
|
||||
} else {
|
||||
return offset + data_vaddr - numbytes_text_and_ro;
|
||||
}
|
||||
});
|
||||
uint32_t sub_type = unpatched_word >> 28;
|
||||
|
||||
if (patch_kind == 0) {
|
||||
assert(sub_type == 0);
|
||||
*patch_ptr = virtual_address;
|
||||
} else {
|
||||
assert(sub_type < 2);
|
||||
|
||||
uint32_t offset = text_vaddr + sizeof(*patch_ptr) * (patch_ptr - segment_ptrs[0]);
|
||||
if (sub_type == 0) {
|
||||
*patch_ptr = virtual_address - offset;
|
||||
} else {
|
||||
*patch_ptr = (virtual_address - offset) & 0x7fffffff;
|
||||
}
|
||||
}
|
||||
|
||||
patch_ptr++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Eh.
|
||||
std::vector<uint8_t> program_data8(total_size, 0);
|
||||
std::memcpy(program_data8.data(), program_data.data(), total_size);
|
||||
return program_data8;
|
||||
}
|
||||
|
||||
HLE::OS::OS::ResultAnd<uint32_t> Read(HLE::PXI::FS::FileContext& context, uint64_t offset, uint32_t num_bytes, HLE::PXI::FS::FileBuffer&& dest) override {
|
||||
FileFormat::Dot3DSX::Header dot3dsxheader = Read3DSXHeader(context);
|
||||
std::optional<FileFormat::Dot3DSX::SecondaryHeader> dot3dsx_secondary_header = Read3DSXSecondaryHeader(context, dot3dsxheader.header_size);
|
||||
|
||||
// Build ExeFS header
|
||||
FileFormat::ExeFSHeader exefsheader = {};
|
||||
ranges::copy(".code", exefsheader.files[0].name.begin());
|
||||
exefsheader.files[0].size_bytes = GetProgramCodeSize(dot3dsxheader);
|
||||
|
||||
// Offsets within the exposed NCCH file
|
||||
const uint64_t ncch_offset = 0;
|
||||
const uint64_t exheader_offset = ncch_offset + sizeof(FileFormat::NCCHHeader);
|
||||
const uint64_t exefsheader_offset = exheader_offset + sizeof(FileFormat::ExHeader); // TODO: ncch.exefs_offset.ToBytes() ?
|
||||
const uint64_t exefsdata_offset = exefsheader_offset + sizeof(FileFormat::ExeFSHeader);
|
||||
|
||||
// +0x1ff: Round up to the next media unit size
|
||||
ncch.exefs_offset = FileFormat::MediaUnit32::FromBytes(exefsheader_offset);
|
||||
ncch.exefs_size = FileFormat::MediaUnit32::FromBytes(exefsheader.GetExeFSSize() + 0x1ff);
|
||||
if (dot3dsx_secondary_header) {
|
||||
ncch.romfs_offset = FileFormat::MediaUnit32::FromBytes(0x1ff + ncch.exefs_offset.ToBytes() + ncch.exefs_size.ToBytes());
|
||||
auto [result, total_size] = file->GetSize(context);
|
||||
if (result != HLE::OS::RESULT_OK) {
|
||||
throw std::runtime_error("Failed to get own file size");
|
||||
}
|
||||
ncch.romfs_size = FileFormat::MediaUnit32::FromBytes(total_size - dot3dsx_secondary_header->romfs_offset + 0x1ff + 0x1000); // Offset by 0x1000 to get to the level3 image (the only part contained in 3dsx files)
|
||||
}
|
||||
|
||||
if (offset >= ncch_offset && offset + num_bytes <= exheader_offset) {
|
||||
// TODO: Use FileFormat::Save instead!
|
||||
dest.Write(reinterpret_cast<char*>(&ncch) + (offset - ncch_offset), num_bytes);
|
||||
} else if (offset >= exheader_offset && offset < exheader_offset + sizeof(exheader)) {
|
||||
if (offset != exheader_offset || num_bytes != sizeof(exheader)) {
|
||||
throw std::runtime_error("Cannot partially read extended header for 3DSX files currently");
|
||||
}
|
||||
|
||||
// Patch 3DSX data
|
||||
uint32_t text_numpages = ((dot3dsxheader.text_size +0xfff) >> 12);
|
||||
uint32_t ro_numpages = ((dot3dsxheader.ro_size+0xfff) >> 12);
|
||||
uint32_t data_numpages = (((dot3dsxheader.data_bss_size - dot3dsxheader.bss_size) + 0xfff) >> 12);
|
||||
|
||||
uint32_t text_vaddr = 0x00100000;
|
||||
uint32_t ro_vaddr = text_vaddr + (text_numpages << 12);
|
||||
uint32_t data_vaddr = ro_vaddr + (ro_numpages << 12);
|
||||
|
||||
ranges::copy("3DSXCart", exheader.application_title.begin());
|
||||
exheader.unknown = {};
|
||||
exheader.flags = {};//inject_target.exheader.flags.compress_exefs_code().Set(0);
|
||||
exheader.remaster_version = {};
|
||||
exheader.section_text = FileFormat::ExHeader::CodeSetInfo{text_vaddr, text_numpages, dot3dsxheader.text_size };
|
||||
exheader.section_ro = FileFormat::ExHeader::CodeSetInfo{ro_vaddr, ro_numpages, dot3dsxheader.ro_size };
|
||||
exheader.section_data = FileFormat::ExHeader::CodeSetInfo{data_vaddr, data_numpages, dot3dsxheader.data_bss_size - dot3dsxheader.bss_size };
|
||||
exheader.bss_size = dot3dsxheader.bss_size;
|
||||
exheader.stack_size = 0x4000; // TODO: Should we adjust this to some other value?
|
||||
exheader.unknown3 = {};
|
||||
exheader.unknown4 = {};
|
||||
// TODO: Use FileFormat::Save instead!
|
||||
dest.Write(reinterpret_cast<char*>(&exheader), sizeof(exheader));
|
||||
} else if (offset >= exefsheader_offset && offset < exefsheader_offset + sizeof(exefsheader)) {
|
||||
if (offset != exefsheader_offset || num_bytes != sizeof(exefsheader)) {
|
||||
throw Mikage::Exceptions::NotImplemented( "Cannot partially read ExeFS header for 3DSX files currently (header at {:#x}-{:#x}, requested {:#x}-{:#x})",
|
||||
exefsheader_offset, exefsheader_offset + sizeof(exefsheader), offset, offset + num_bytes);
|
||||
} else {
|
||||
// TODO: Use FileFormat::Save instead!
|
||||
dest.Write(reinterpret_cast<char*>(&exefsheader), sizeof(exefsheader));
|
||||
}
|
||||
} else if (offset >= exefsdata_offset && offset < exefsdata_offset + GetProgramCodeSize(dot3dsxheader)) {
|
||||
if (offset != exefsdata_offset || num_bytes != GetProgramCodeSize(dot3dsxheader)) {
|
||||
throw std::runtime_error("Cannot partially read ExeFS data for 3DSX files currently");
|
||||
}
|
||||
|
||||
auto program_code = ReadProgramCode(context, dot3dsxheader);
|
||||
dest.Write(reinterpret_cast<char*>(program_code.data()), program_code.size());
|
||||
} else if (offset >= ncch.romfs_offset.ToBytes() + 0x1000 && offset + num_bytes <= ncch.romfs_offset.ToBytes() + ncch.romfs_size.ToBytes()) {
|
||||
if (!dot3dsx_secondary_header) {
|
||||
throw std::runtime_error("Tried reading RomFS even though no RomFS is present");
|
||||
}
|
||||
|
||||
if (offset - ncch.romfs_offset.ToBytes() < 0x1000) {
|
||||
// TODO: offset_within_romfs would end up negative in this case, but I'm not sure this condition will ever be hit in practice.
|
||||
throw std::runtime_error("Unimplemented case");
|
||||
}
|
||||
auto offset_within_romfs = offset - ncch.romfs_offset.ToBytes() - 0x1000; // Offset by 0x1000 to get to the level3 image (the only part contained in 3dsx files)
|
||||
file->Read(context, dot3dsx_secondary_header->romfs_offset + offset_within_romfs, num_bytes, std::move(dest));
|
||||
} else {
|
||||
throw std::runtime_error(fmt::format("Unsupported read range: Tried reading {:#x} bytes from offset {:#x}", num_bytes, offset));
|
||||
}
|
||||
|
||||
return std::make_tuple(HLE::OS::RESULT_OK, num_bytes);
|
||||
}
|
||||
|
||||
HLE::OS::OS::ResultAnd<uint64_t> GetSize(HLE::PXI::FS::FileContext& context) override {
|
||||
// Return original file size as upper bound; this is merely needed to prevent the bounds check in FileViews from failing
|
||||
return file->GetSize(context);
|
||||
}
|
||||
|
||||
void Close() override {
|
||||
file->Close();
|
||||
}
|
||||
};
|
||||
|
||||
} // end of anonymous namespace
|
||||
|
||||
GameCardFrom3DSX::GameCardFrom3DSX(std::string_view filename) : source(std::string { filename }) {
|
||||
}
|
||||
|
||||
GameCardFrom3DSX::GameCardFrom3DSX(int file_descriptor) : source(file_descriptor) {
|
||||
}
|
||||
|
||||
std::optional<std::unique_ptr<HLE::PXI::FS::File>> GameCardFrom3DSX::GetPartitionFromId(Loader::NCSDPartitionId id) {
|
||||
if (id == NCSDPartitionId::Executable) {
|
||||
auto file = new Adaptor3DSXToNCCH(std::visit(GameCardSourceOpener{}, source));
|
||||
return std::unique_ptr<HLE::PXI::FS::File>(file);
|
||||
} else {
|
||||
throw std::runtime_error("Not implemented yet");
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
static bool IsLoadable3DSXFile(HLE::PXI::FS::File& file) {
|
||||
// TODO: Enable proper logging
|
||||
auto logger = std::make_shared<spdlog::logger>("dummy", std::make_shared<spdlog::sinks::null_sink_st>());
|
||||
auto file_context = HLE::PXI::FS::FileContext { *logger };
|
||||
file.Open(file_context, false);
|
||||
|
||||
FileFormat::Dot3DSX::Header header;
|
||||
auto [result, bytes_read] = file.Read(file_context, 0, sizeof(header), HLE::PXI::FS::FileBufferInHostMemory(header));
|
||||
if (result != HLE::OS::RESULT_OK || bytes_read != sizeof(header)) {
|
||||
return false;
|
||||
}
|
||||
return ranges::equal(header.magic, std::string_view { "3DSX" });
|
||||
}
|
||||
|
||||
bool GameCardFrom3DSX::IsLoadableFile(std::string_view filename) {
|
||||
auto file = HLE::PXI::FS::HostFile(filename, {});
|
||||
return IsLoadable3DSXFile(file);
|
||||
}
|
||||
|
||||
bool GameCardFrom3DSX::IsLoadableFile(int file_descriptor) {
|
||||
return IsLoadable3DSXFile(*FileSystem::OpenNativeFile(file_descriptor));
|
||||
}
|
||||
|
||||
} // namespace Loader
|
90
source/loader/gamecard.hpp
Normal file
90
source/loader/gamecard.hpp
Normal file
|
@ -0,0 +1,90 @@
|
|||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
|
||||
namespace HLE {
|
||||
namespace PXI {
|
||||
namespace FS {
|
||||
class Archive;
|
||||
class File;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace Loader {
|
||||
|
||||
/// TODO: This is what's called "Content Index" in other places
|
||||
enum class NCSDPartitionId {
|
||||
Executable = 0,
|
||||
Manual = 1,
|
||||
DownloadPlayChild = 2,
|
||||
UpdateDataNew3D = 6,
|
||||
UpdateData = 7
|
||||
};
|
||||
|
||||
/**
|
||||
* Top-level game card interface.
|
||||
*/
|
||||
class GameCard {
|
||||
public:
|
||||
/**
|
||||
* Return a pointer to a File from which the partition's NCCH data can be accessed, or std::nullopt if the partition doesn't exist.
|
||||
*/
|
||||
virtual std::optional<std::unique_ptr<HLE::PXI::FS::File>> GetPartitionFromId(NCSDPartitionId id) = 0;
|
||||
|
||||
virtual ~GameCard();
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility to wrap a CXI container into a single-partition GameCard.
|
||||
*/
|
||||
class GameCardFromCXI final : public GameCard {
|
||||
std::optional<std::unique_ptr<HLE::PXI::FS::File>> GetPartitionFromId(NCSDPartitionId id) override;
|
||||
|
||||
// Filename or file descriptor
|
||||
std::variant<std::string, int> source;
|
||||
|
||||
public:
|
||||
GameCardFromCXI(std::string_view filename);
|
||||
GameCardFromCXI(int file_descriptor);
|
||||
|
||||
static bool IsLoadableFile(std::string_view filename);
|
||||
static bool IsLoadableFile(int file_descriptor);
|
||||
};
|
||||
|
||||
class GameCardFromCCI final : public GameCard {
|
||||
std::optional<std::unique_ptr<HLE::PXI::FS::File>> GetPartitionFromId(NCSDPartitionId id) override;
|
||||
|
||||
// Filename or file descriptor
|
||||
std::variant<std::string, int> source;
|
||||
|
||||
public:
|
||||
GameCardFromCCI(std::string_view filename);
|
||||
GameCardFromCCI(int file_descriptor);
|
||||
|
||||
static bool IsLoadableFile(std::string_view filename);
|
||||
static bool IsLoadableFile(int file_descriptor);
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility to wrap a 3DSX file into a single-partition GameCard.
|
||||
*/
|
||||
class GameCardFrom3DSX final : public GameCard {
|
||||
std::optional<std::unique_ptr<HLE::PXI::FS::File>> GetPartitionFromId(NCSDPartitionId id) override;
|
||||
|
||||
// Filename or file descriptor
|
||||
std::variant<std::string, int> source;
|
||||
|
||||
public:
|
||||
GameCardFrom3DSX(std::string_view filename);
|
||||
GameCardFrom3DSX(int file_descriptor);
|
||||
|
||||
static bool IsLoadableFile(std::string_view filename);
|
||||
static bool IsLoadableFile(int file_descriptor);
|
||||
};
|
||||
|
||||
} // namespace Loader
|
46
source/loader/host_file.cpp
Normal file
46
source/loader/host_file.cpp
Normal file
|
@ -0,0 +1,46 @@
|
|||
#include "host_file.hpp"
|
||||
|
||||
#include <boost/iostreams/device/file_descriptor.hpp>
|
||||
#include <boost/iostreams/operations.hpp>
|
||||
|
||||
namespace FileSystem {
|
||||
|
||||
class NativeFile final : public HLE::PXI::FS::File {
|
||||
public:
|
||||
boost::iostreams::file_descriptor_source source;
|
||||
|
||||
public:
|
||||
NativeFile(int file_descriptor) : source(file_descriptor, boost::iostreams::never_close_handle) {
|
||||
}
|
||||
|
||||
HLE::OS::ResultAnd<> Open(HLE::PXI::FS::FileContext&, bool create) override {
|
||||
// Nothing to do
|
||||
return std::make_tuple(HLE::OS::RESULT_OK);
|
||||
}
|
||||
|
||||
HLE::OS::ResultAnd<uint64_t> GetSize(HLE::PXI::FS::FileContext&) override {
|
||||
auto begin = boost::iostreams::seek(source, 0, std::ios_base::beg);
|
||||
auto end = boost::iostreams::seek(source, 0, std::ios_base::end);
|
||||
return std::make_tuple(HLE::OS::RESULT_OK, static_cast<uint64_t>(end - begin));
|
||||
}
|
||||
|
||||
HLE::OS::ResultAnd<uint32_t> Read(HLE::PXI::FS::FileContext& context, uint64_t offset, uint32_t num_bytes, HLE::PXI::FS::FileBuffer&& dest) override {
|
||||
boost::iostreams::seek(source, offset, std::ios_base::beg);
|
||||
|
||||
std::vector<char> data;
|
||||
data.resize(num_bytes);
|
||||
boost::iostreams::read(source, data.data(), num_bytes);
|
||||
dest.Write(data.data(), num_bytes);
|
||||
return std::make_tuple(HLE::OS::RESULT_OK, num_bytes);
|
||||
}
|
||||
|
||||
void Close() override {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<HLE::PXI::FS::File> OpenNativeFile(int file_desctiptor) {
|
||||
return std::unique_ptr<HLE::PXI::FS::File>(new NativeFile(file_desctiptor));
|
||||
}
|
||||
|
||||
} // namespace FileSystem
|
9
source/loader/host_file.hpp
Normal file
9
source/loader/host_file.hpp
Normal file
|
@ -0,0 +1,9 @@
|
|||
#pragma once
|
||||
|
||||
#include "../processes/pxi_fs.hpp"
|
||||
|
||||
namespace FileSystem {
|
||||
|
||||
std::unique_ptr<HLE::PXI::FS::File> OpenNativeFile(int file_desctiptor);
|
||||
|
||||
} // namespace FileSystem
|
1587
source/memory.cpp
Normal file
1587
source/memory.cpp
Normal file
File diff suppressed because it is too large
Load diff
612
source/memory.h
Normal file
612
source/memory.h
Normal file
|
@ -0,0 +1,612 @@
|
|||
/**
|
||||
* @file Memory interface used to represent and access emulated memory and
|
||||
* MMIO devices.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <spdlog/fmt/fmt.h>
|
||||
|
||||
#include <boost/endian/conversion.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
class LogManager;
|
||||
|
||||
class PicaContext;
|
||||
|
||||
class InputSource;
|
||||
|
||||
namespace EmuDisplay {
|
||||
struct EmuDisplay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Memory namespace
|
||||
*/
|
||||
namespace Memory {
|
||||
|
||||
// TODO: Change this to a strongly typed enum to aid the compiler optimizer in pointer analysis
|
||||
using EmulatedMemory = uint8_t;
|
||||
|
||||
using PAddr = uint32_t;
|
||||
|
||||
struct HostMemoryBackedPage {
|
||||
// nullptr if no memory backed page exists
|
||||
EmulatedMemory* data = nullptr;
|
||||
|
||||
explicit operator bool() const {
|
||||
return (data != nullptr);
|
||||
}
|
||||
};
|
||||
|
||||
// Set of contiguous pages in memory backed by host RAM
|
||||
struct HostMemoryBackedPages {
|
||||
EmulatedMemory* data = nullptr;
|
||||
|
||||
uint32_t num_bytes = 0;
|
||||
|
||||
explicit operator bool() const {
|
||||
return (data != nullptr);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Type representing a the physical address range of a memory bus.
|
||||
*/
|
||||
template<uint32_t PAddrStart, uint32_t Size>
|
||||
struct Bus {
|
||||
static constexpr uint32_t start = PAddrStart;
|
||||
static constexpr uint32_t size = Size;
|
||||
static constexpr uint32_t end = start + size;
|
||||
|
||||
uint8_t Read8(uint32_t address);
|
||||
uint16_t Read16(uint32_t address);
|
||||
uint32_t Read32(uint32_t address);
|
||||
|
||||
void Write8(uint32_t address, uint8_t value);
|
||||
void Write16(uint32_t address, uint16_t value);
|
||||
void Write32(uint32_t address, uint32_t value);
|
||||
|
||||
static_assert(start < static_cast<uint32_t>(start + size), "End address wrapped due to overflow... wrong template parameters?");
|
||||
};
|
||||
|
||||
enum class HookKind {
|
||||
Read,
|
||||
Write,
|
||||
ReadWrite
|
||||
};
|
||||
|
||||
constexpr bool HasReadHook(HookKind kind) noexcept {
|
||||
return (kind == HookKind::Read || kind == HookKind::ReadWrite);
|
||||
}
|
||||
|
||||
constexpr bool HasWriteHook(HookKind kind) noexcept {
|
||||
return (kind == HookKind::Write || kind == HookKind::ReadWrite);
|
||||
}
|
||||
|
||||
constexpr HookKind AddReadHook(HookKind kind) noexcept {
|
||||
return (kind == HookKind::Write) ? HookKind::ReadWrite : HookKind::Read;
|
||||
}
|
||||
|
||||
constexpr HookKind AddWriteHook(HookKind kind) noexcept {
|
||||
return (kind == HookKind::Read) ? HookKind::ReadWrite : HookKind::Write;
|
||||
}
|
||||
|
||||
template<HookKind>
|
||||
struct Hook;
|
||||
|
||||
using PAddr = uint32_t;
|
||||
|
||||
struct MemoryRange {
|
||||
PAddr start;
|
||||
uint32_t num_bytes;
|
||||
};
|
||||
|
||||
struct HookBase {
|
||||
// Actual memory range this hook is active for
|
||||
MemoryRange range;
|
||||
|
||||
// Other hook contained in this same page
|
||||
// Guaranteed to be non-overlapping and to cover only memory *after* this hook
|
||||
std::unique_ptr<HookBase> next {};
|
||||
};
|
||||
|
||||
template<HookKind>
|
||||
struct HookHandler;
|
||||
|
||||
template<> struct HookHandler<HookKind::Read> {
|
||||
virtual void OnRead(PAddr addr, uint32_t num_bytes) = 0;
|
||||
};
|
||||
|
||||
template<> struct HookHandler<HookKind::Write> {
|
||||
virtual void OnWrite(PAddr addr, uint32_t num_bytes, uint32_t value) = 0;
|
||||
};
|
||||
|
||||
using ReadHandler = HookHandler<HookKind::Read>;
|
||||
using WriteHandler = HookHandler<HookKind::Write>;
|
||||
|
||||
// Arguments: Address and value size
|
||||
template<>
|
||||
struct Hook<HookKind::Read> : HookBase {
|
||||
Hook(MemoryRange range, ReadHandler& handler_) : HookBase { range }, handler(handler_) {}
|
||||
|
||||
ReadHandler& handler;
|
||||
};
|
||||
|
||||
// Arguments: Address, value, and value size
|
||||
template<>
|
||||
struct Hook<HookKind::Write> : HookBase {
|
||||
Hook(MemoryRange range, WriteHandler& handler_) : HookBase { range }, handler(handler_) {}
|
||||
|
||||
WriteHandler& handler;
|
||||
};
|
||||
|
||||
using ReadHook = Hook<HookKind::Read>;
|
||||
using WriteHook = Hook<HookKind::Write>;
|
||||
|
||||
// TODO: Find optimal alignment (check VEC_SIZE in glibc? Might just be 64 for AVX2). Having any alignment at all helps the compiler generate better memset()s for initialization
|
||||
template<uint32_t PAddrStart, uint32_t Size>
|
||||
struct alignas(512) MemoryBus : Bus<PAddrStart, Size> {
|
||||
// NOTE: If we over-allocate this array (by adding 3 more entries than strictly necessary), we can avoid needing range checks! (or at least we can reshuffle them past the actual writes to benefit from speculative execution)
|
||||
uint8_t data[Size];
|
||||
|
||||
std::array<std::unique_ptr<HookBase>, (Size >> 12)> write_hooks;
|
||||
std::array<std::unique_ptr<HookBase>, (Size >> 12)> read_hooks;
|
||||
|
||||
// TODO: When no hooks are set, callers should always use direct memory
|
||||
// access instead of calling the Read/Write* functions. Currently,
|
||||
// ReadLegacy/WriteLegacy is used in many places still, but once
|
||||
// that is deprecated we should assert that a hook is set when
|
||||
// using Write*.
|
||||
|
||||
uint8_t Read8(uint32_t address) {
|
||||
assert(address < this->end);
|
||||
if (auto* hook = LookupReadHookFor(address)) {
|
||||
hook->handler.OnRead(address, sizeof(uint8_t));
|
||||
} else {
|
||||
// TODO: See above
|
||||
}
|
||||
return data[address - PAddrStart];
|
||||
}
|
||||
uint16_t Read16(uint32_t address) {
|
||||
if (auto* hook = LookupReadHookFor(address)) {
|
||||
hook->handler.OnRead(address, sizeof(uint16_t));
|
||||
} else {
|
||||
// TODO: See above
|
||||
}
|
||||
|
||||
// TODO: Endianness
|
||||
uint16_t ret;
|
||||
memcpy(&ret, &data[address - PAddrStart], sizeof(ret));
|
||||
return ret;
|
||||
}
|
||||
uint32_t Read32(uint32_t address) {
|
||||
if (auto* hook = LookupReadHookFor(address)) {
|
||||
hook->handler.OnRead(address, sizeof(uint32_t));
|
||||
} else {
|
||||
// TODO: See above
|
||||
}
|
||||
|
||||
// TODO: Endianness
|
||||
uint32_t ret;
|
||||
memcpy(&ret, &data[address - PAddrStart], sizeof(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
void Write8(uint32_t address, uint8_t value) {
|
||||
assert(address < this->end);
|
||||
if (auto* hook = LookupWriteHookFor(address)) {
|
||||
hook->handler.OnWrite(address, sizeof(value), value);
|
||||
} else {
|
||||
// TODO: See above
|
||||
}
|
||||
data[address - PAddrStart] = value;
|
||||
}
|
||||
void Write16(uint32_t address, uint16_t value) {
|
||||
if (auto* hook = LookupWriteHookFor(address)) {
|
||||
hook->handler.OnWrite(address, sizeof(value), value);
|
||||
} else {
|
||||
// TODO: See above
|
||||
}
|
||||
// TODO: Endianness
|
||||
memcpy(&data[address - PAddrStart], &value, sizeof(value));
|
||||
}
|
||||
void Write32(uint32_t address, uint32_t value) {
|
||||
if (auto* hook = LookupWriteHookFor(address)) {
|
||||
hook->handler.OnWrite(address, sizeof(value), value);
|
||||
} else {
|
||||
// TODO: See above
|
||||
}
|
||||
// TODO: Endianness
|
||||
memcpy(&data[address - PAddrStart], &value, sizeof(value));
|
||||
}
|
||||
|
||||
private:
|
||||
friend HostMemoryBackedPages LookupContiguousMemoryBackedPage(struct PhysicalMemory&, PAddr, uint32_t);
|
||||
|
||||
template<HookKind AccessMode>
|
||||
friend HostMemoryBackedPages LookupContiguousMemoryBackedPage(struct PhysicalMemory&, PAddr, uint32_t);
|
||||
|
||||
WriteHook* LookupWriteHookFor(uint32_t address) noexcept {
|
||||
return static_cast<WriteHook*>(write_hooks[(address - PAddrStart) >> 12].get());
|
||||
}
|
||||
|
||||
ReadHook* LookupReadHookFor(uint32_t address) noexcept {
|
||||
return static_cast<ReadHook*>(read_hooks[(address - PAddrStart) >> 12].get());
|
||||
}
|
||||
};
|
||||
|
||||
struct MemoryAccessHandler {
|
||||
uint8_t Read8(uint32_t offset);
|
||||
uint16_t Read16(uint32_t offset);
|
||||
uint32_t Read32(uint32_t offset);
|
||||
|
||||
void Write8(uint32_t offset, uint8_t value);
|
||||
void Write16(uint32_t offset, uint16_t value);
|
||||
void Write32(uint32_t offset, uint32_t value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Proxy bus descriptor structure that forwards all reads and writes to a
|
||||
* handler given by T. This allows us to define MMIO handlers in external
|
||||
* translation units and forward-declaring them in this file instead of
|
||||
* defining them all in this header.
|
||||
* @tparam T handler structure to forward Read/Write function calls to. Must be a base of MemoryAccessHandler
|
||||
*/
|
||||
template<typename T, uint32_t PAddrStart, uint32_t Size>
|
||||
struct ProxyBus : MemoryBus<PAddrStart, Size> {
|
||||
std::unique_ptr<T> handler;
|
||||
|
||||
// Constructors and destructors are defined outside the header since T is
|
||||
// not a complete type in most translation units (which is necessary to
|
||||
// implement this destructor).
|
||||
ProxyBus(std::unique_ptr<T> handler);
|
||||
ProxyBus(ProxyBus&& bus);
|
||||
~ProxyBus();
|
||||
|
||||
uint8_t Read8(uint32_t address);
|
||||
uint16_t Read16(uint32_t address);
|
||||
uint32_t Read32(uint32_t address);
|
||||
|
||||
void Write8(uint32_t address, uint8_t value);
|
||||
void Write16(uint32_t address, uint16_t value);
|
||||
void Write32(uint32_t address, uint32_t value);
|
||||
};
|
||||
|
||||
struct MPCorePrivate;
|
||||
struct HID;
|
||||
struct LCD;
|
||||
struct AXIHandler;
|
||||
struct GPU;
|
||||
struct HASH;
|
||||
struct GPIO;
|
||||
struct SPI;
|
||||
struct CONFIG11;
|
||||
struct DSPMMIO;
|
||||
|
||||
struct DSPMemory;
|
||||
|
||||
using VRAM = MemoryBus<0x18000000, 0x00600000>;
|
||||
//using DSP = MemoryBus<0x1ff00000, 0x00080000>;
|
||||
using DSP = ProxyBus<DSPMemory, 0x1ff00000, 0x00080000>;
|
||||
using AXI = MemoryBus<0x1ff80000, 0x00080000>;
|
||||
//using FCRAM = MemoryBus<0x20000000, 0x08000000>;
|
||||
// Upper 0x08000000 bytes only available on New3DS!
|
||||
// using FCRAM = MemoryBus<0x20000000, 0x10000000>;
|
||||
using FCRAM = MemoryBus<0x20000000, 0x0800'0000>;
|
||||
|
||||
using IO_HASH = ProxyBus<HASH, 0x10101000, 0x1000>;
|
||||
using IO_DSP1 = ProxyBus<DSPMMIO, 0x10103000, 0x1000>; // TODO: Actually CSND registers
|
||||
|
||||
using IO_CONFIG11 = ProxyBus<CONFIG11, 0x10140000, 0x2000>;
|
||||
using IO_SPIBUS2 = ProxyBus<SPI, 0x10142000, 0x1000>; // SPI devices 3/4/5
|
||||
using IO_SPIBUS3 = ProxyBus<SPI, 0x10143000, 0x1000>; // SPI device 6
|
||||
using IO_HID = ProxyBus<HID, 0x10146000, 0x1000>;
|
||||
using IO_GPIO = ProxyBus<GPIO, 0x10147000, 0x1000>;
|
||||
|
||||
using IO_SPIBUS1 = ProxyBus<SPI, 0x10160000, 0x1000>; // SPI devices 0/1/2 (3/4/5 at 0x10142000, 6 at 0x10143000)
|
||||
|
||||
using IO_LCD = ProxyBus<LCD, 0x10202000, 0x1000>;
|
||||
using IO_DSP2 = ProxyBus<DSPMMIO, 0x10203000, 0x1000>; // TODO: Is this indeed just a mirror of 0x10103000? TODO: Actually the real DSP registers
|
||||
using IO_AXI = ProxyBus<AXIHandler, 0x1020F000, 0x1000>;
|
||||
|
||||
using IO_HASH2 = ProxyBus<HASH, 0x10301000, 0x1000>; // NOTE: This is *NOT* just a mirror of 0x10101000 ... (3dbrew erratum)
|
||||
|
||||
using IO_GPU = ProxyBus<GPU, 0x10400000, 0x2000>;
|
||||
|
||||
using MPCorePrivateBus = ProxyBus<MPCorePrivate, 0x17e00000, 0x00002000>;
|
||||
|
||||
|
||||
struct PhysicalMemorySubscriber;
|
||||
|
||||
struct PhysicalMemory {
|
||||
using Busses = std::tuple<FCRAM, VRAM, DSP, AXI, IO_HID, IO_LCD, IO_AXI,
|
||||
IO_GPU, IO_HASH,
|
||||
IO_CONFIG11, IO_DSP1,
|
||||
IO_SPIBUS2, IO_SPIBUS3,
|
||||
IO_HASH2, IO_GPIO, IO_SPIBUS1,
|
||||
IO_DSP2, MPCorePrivateBus>;
|
||||
|
||||
Busses memory;
|
||||
|
||||
std::vector<PhysicalMemorySubscriber*> subscribers;
|
||||
|
||||
/**
|
||||
* @note This constructor leaves the memory system in a partially
|
||||
* unintialized state. To finalize initialization, some of the busses
|
||||
* need to be connected to their respective end points.
|
||||
*/
|
||||
PhysicalMemory(LogManager& log_manager);
|
||||
|
||||
void InjectDependency(PicaContext& pica);
|
||||
void InjectDependency(InputSource&);
|
||||
};
|
||||
|
||||
struct PhysicalMemorySubscriber {
|
||||
PhysicalMemorySubscriber(PhysicalMemory& mem_) : mem(mem_) {
|
||||
mem.subscribers.push_back(this);
|
||||
}
|
||||
|
||||
PhysicalMemory& mem;
|
||||
|
||||
virtual ~PhysicalMemorySubscriber() {
|
||||
for (auto it = mem.subscribers.begin(); it != mem.subscribers.end(); ++it) {
|
||||
if (*it == this) {
|
||||
mem.subscribers.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Called when a MemoryBus in PhysicalMemory transitions from
|
||||
// "not backed by host memory" to "backed by host memory" state
|
||||
virtual void OnBackedByHostMemory(HostMemoryBackedPage page, uint32_t address) {
|
||||
// Do nothing by default
|
||||
}
|
||||
|
||||
// Called when a MemoryBus in PhysicalMemory transitions from
|
||||
// "backed by host memory" to "not backed by host memory" state
|
||||
virtual void OnUnbackedByHostMemory(uint32_t address) {
|
||||
// Do nothing by default
|
||||
}
|
||||
};
|
||||
|
||||
struct IsInside {
|
||||
uint32_t address;
|
||||
|
||||
template<typename Bus>
|
||||
bool operator()(const Bus& bus) const {
|
||||
return address >= bus.start && address < bus.end;
|
||||
}
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
|
||||
/// Utility functor to call the appropriate Read8/Read16/Read32 variant based on the given data type
|
||||
template<typename DataType>
|
||||
struct BusReader {
|
||||
uint32_t address;
|
||||
|
||||
template<typename Bus>
|
||||
DataType operator()(Bus& bus) const {
|
||||
static_assert(std::is_same_v<DataType, uint8_t> || std::is_same_v<DataType, uint16_t> || std::is_same_v<DataType, uint32_t>, "Invalid DataType");
|
||||
if constexpr (std::is_same<DataType, uint8_t>::value) {
|
||||
return bus.Read8(address);
|
||||
} else if constexpr (std::is_same<DataType, uint16_t>::value) {
|
||||
return bus.Read16(address);
|
||||
} else if constexpr (std::is_same<DataType, uint32_t>::value) {
|
||||
return bus.Read32(address);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Utility functor to call the appropriate Read8/Read16/Read32 variant based on the given data type
|
||||
template<typename DataType>
|
||||
struct BusWriter {
|
||||
uint32_t address;
|
||||
|
||||
template<typename Bus>
|
||||
void operator()(Bus& bus, DataType value) const {
|
||||
static_assert(std::is_same_v<DataType, uint8_t> || std::is_same_v<DataType, uint16_t> || std::is_same_v<DataType, uint32_t>, "Invalid DataType");
|
||||
if (std::is_same<DataType, uint8_t>::value) {
|
||||
return bus.Write8(address, value);
|
||||
} else if (std::is_same<DataType, uint16_t>::value) {
|
||||
return bus.Write16(address, value);
|
||||
} else if (std::is_same<DataType, uint32_t>::value) {
|
||||
return bus.Write32(address, value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template<typename DataType, typename BusTuple, size_t... Idxs>
|
||||
DataType ReadHelper(PhysicalMemory& mem, uint32_t address, std::index_sequence<Idxs...>) {
|
||||
DataType data;
|
||||
auto attempt_read = [&](auto&& bus) {
|
||||
if (IsInside{address}(bus)) {
|
||||
data = BusReader<DataType>{address}(bus);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Using short-circuit evaluation here to keep testing until we find the right bus (but no further than that)
|
||||
bool match_found = (attempt_read(std::get<Idxs>(mem.memory)) || ...);
|
||||
if (!match_found)
|
||||
throw std::runtime_error(fmt::format("Read from unknown physical address {:#010x}", address));
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
template<typename DataType, typename BusTuple, size_t... Idxs>
|
||||
void WriteHelper(PhysicalMemory& mem, uint32_t address, DataType value, std::index_sequence<Idxs...>) {
|
||||
auto attempt_write = [&](auto&& bus) {
|
||||
if (IsInside{address}(bus)) {
|
||||
BusWriter<DataType>{address}(bus, value);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Using short-circuit evaluation here to keep testing until we find the right bus (but no further than that)
|
||||
bool match_found = (attempt_write(std::get<Idxs>(mem.memory)) || ...);
|
||||
if (!match_found)
|
||||
throw std::runtime_error(fmt::format("Write to unknown physical address {:#010x}", address));
|
||||
}
|
||||
|
||||
template<typename Bus, typename Callback>
|
||||
bool ForEachMemoryBusHelper(Bus& bus, Callback& callback) {
|
||||
if constexpr (std::is_base_of_v<MemoryBus<Bus::start, Bus::size>, Bus>) {
|
||||
return callback(bus);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Iterates over all MemoryBusses, calling callback on each of them until it returns true
|
||||
template<typename Callback, typename... Busses>
|
||||
bool ForEachMemoryBus(std::tuple<Busses...>& busses, Callback& callback) {
|
||||
return (ForEachMemoryBusHelper(std::get<Busses>(busses), callback) || ...);
|
||||
}
|
||||
|
||||
template<typename Bus>
|
||||
HostMemoryBackedPage GetMemoryBackedPageFor(Bus& bus, PAddr address) {
|
||||
return { bus.data + (address - bus.start) };
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/**
|
||||
* Read from PhysicalMemory under the constraint that only the address ranges on
|
||||
* the BusTuple are considered.
|
||||
* @throws std::runtime_error when the given address is outside any of the
|
||||
* given Bus address ranges
|
||||
*/
|
||||
template<typename DataType, typename BusTuple = PhysicalMemory::Busses>
|
||||
DataType ReadLegacy(PhysicalMemory& mem, uint32_t address) {
|
||||
// TODO: Statically assert that BusTuple is a subtuple of mem.busses
|
||||
// TODO: Restrict search to the given BusTuple
|
||||
constexpr size_t length = std::tuple_size<BusTuple>::value;
|
||||
return detail::ReadHelper<DataType, BusTuple>(mem, address, std::make_index_sequence<length>{});
|
||||
}
|
||||
|
||||
/**
|
||||
* Write to PhysicalMemory under the constraint that only the address ranges on
|
||||
* the BusTuple are considered.
|
||||
* @throws std::runtime_error when the given address is outside any of the
|
||||
* given Bus address ranges
|
||||
*/
|
||||
template<typename DataType, typename BusTuple = PhysicalMemory::Busses>
|
||||
void WriteLegacy(PhysicalMemory& mem, uint32_t address, DataType value) {
|
||||
// TODO: Statically assert that BusTuple is a subtuple of mem.busses
|
||||
// TODO: Restrict search to the given BusTuple
|
||||
constexpr size_t length = std::tuple_size<BusTuple>::value;
|
||||
detail::WriteHelper<DataType, BusTuple>(mem, address, value, std::make_index_sequence<length>{});
|
||||
}
|
||||
|
||||
template<uint32_t PAddrStart, uint32_t Size>
|
||||
inline constexpr bool IsMemoryBus(const MemoryBus<PAddrStart, Size>&) {
|
||||
return true;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
inline constexpr bool IsMemoryBus(const T&) {
|
||||
return false;
|
||||
}
|
||||
|
||||
inline HostMemoryBackedPage LookupMemoryBackedPage(PhysicalMemory& mem, PAddr address) {
|
||||
HostMemoryBackedPage out_page = { nullptr };
|
||||
auto callback = [&](auto& bus) {
|
||||
if (IsInside{address}(bus)) {
|
||||
// Return null page if this is not a memory bus
|
||||
// TODO: Also return null page if any hooks are set up
|
||||
// if (!bus.write_hooks[(address - bus.start) >> 12] && !bus.read_hooks[(address - bus.start) >> 12]) {
|
||||
if (IsMemoryBus(bus)) {
|
||||
out_page = detail::GetMemoryBackedPageFor(bus, address);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
detail::ForEachMemoryBus(mem.memory, callback);
|
||||
return out_page;
|
||||
}
|
||||
|
||||
// Deprecated since this doesn't check for or trigger any memory hooks
|
||||
[[deprecated]] HostMemoryBackedPages LookupContiguousMemoryBackedPage(PhysicalMemory& mem, PAddr address, uint32_t num_bytes);
|
||||
|
||||
template<HookKind AccessMode>
|
||||
HostMemoryBackedPages LookupContiguousMemoryBackedPage(PhysicalMemory& mem, PAddr address, uint32_t num_bytes);
|
||||
|
||||
template<typename DataType>
|
||||
DataType Read(HostMemoryBackedPage page, uint32_t offset) noexcept {
|
||||
static_assert(std::is_same_v<DataType, uint8_t> || std::is_same_v<DataType, uint16_t> || std::is_same_v<DataType, uint32_t>, "Invalid DataType");
|
||||
if constexpr (std::is_same_v<DataType, uint8_t>) {
|
||||
return page.data[offset];
|
||||
} else {
|
||||
DataType ret;
|
||||
memcpy(&ret, &page.data[offset], sizeof(ret));
|
||||
return boost::endian::little_to_native(ret);
|
||||
}
|
||||
}
|
||||
|
||||
template<typename DataType>
|
||||
DataType Read(HostMemoryBackedPages pages, uint32_t offset) noexcept {
|
||||
return Read<DataType>(HostMemoryBackedPage { pages.data }, offset);
|
||||
}
|
||||
|
||||
template<typename DataType>
|
||||
void Write(HostMemoryBackedPage page, uint32_t offset, DataType value) noexcept {
|
||||
static_assert(std::is_same_v<DataType, uint8_t> || std::is_same_v<DataType, uint16_t> || std::is_same_v<DataType, uint32_t>, "Invalid DataType");
|
||||
if constexpr (std::is_same_v<DataType, uint8_t>) {
|
||||
page.data[offset] = value;
|
||||
} else {
|
||||
value = boost::endian::native_to_little(value);
|
||||
memcpy(&page.data[offset], &value, sizeof(value));
|
||||
}
|
||||
}
|
||||
|
||||
template<typename DataType>
|
||||
void Write(HostMemoryBackedPages pages, uint32_t offset, DataType value) noexcept {
|
||||
Write(HostMemoryBackedPage { pages.data }, offset, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns the given Hook to all pages in the specified address range.
|
||||
*
|
||||
* @pre The address range may not cross a bus boundary
|
||||
*/
|
||||
template<HookKind Kind>
|
||||
void SetHook(PhysicalMemory&, PAddr start, uint32_t num_bytes, HookHandler<Kind>&);
|
||||
|
||||
template<HookKind Kind, typename F>
|
||||
auto GenerateHooks(PhysicalMemory& mem, PAddr start, uint32_t num_bytes, const F& callback)
|
||||
-> std::enable_if_t<std::is_invocable_r_v<HookHandler<Kind>*, F, PAddr>> {
|
||||
static_assert( Kind == HookKind::Read || Kind == HookKind::Write,
|
||||
"Must pick either read or write hooks");
|
||||
|
||||
uint32_t end_page_index = ((start & 0xfff) + 0x1000 + num_bytes - 1) >> 12;
|
||||
for (uint32_t page_index = 0; page_index < end_page_index; ++page_index) {
|
||||
PAddr page_start = ((start >> 12) + page_index) << 12;
|
||||
// Pass 1 as the region size for SetWriteHook since it's not used for single-page assignments anyway
|
||||
PAddr hook_range_start = page_index == 0 ? start : page_start;
|
||||
PAddr hook_range_end = (page_index == end_page_index - 1) ? start + num_bytes : (page_start + 0x1000);
|
||||
auto hook = callback(page_start);
|
||||
if (hook) {
|
||||
SetHook<Kind>(mem, hook_range_start, (hook_range_end - hook_range_start), *hook);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Should read the expected HookHandler<Kind>& for validation
|
||||
void ClearHook(PhysicalMemory& mem, HookKind kind, PAddr start, uint32_t num_bytes);
|
||||
|
||||
} // namespace Memory
|
6257
source/os.cpp
Normal file
6257
source/os.cpp
Normal file
File diff suppressed because it is too large
Load diff
2048
source/os.hpp
Normal file
2048
source/os.hpp
Normal file
File diff suppressed because it is too large
Load diff
383
source/os_console.cpp
Normal file
383
source/os_console.cpp
Normal file
|
@ -0,0 +1,383 @@
|
|||
#include "os.hpp"
|
||||
#include "os_console.hpp"
|
||||
|
||||
#include <platform/sm.hpp>
|
||||
|
||||
#include <framework/meta_tools.hpp>
|
||||
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
|
||||
#include <boost/hana/ext/boost/fusion/deque.hpp>
|
||||
#include <boost/hana/ext/std/tuple.hpp>
|
||||
|
||||
#include <boost/range/adaptor/filtered.hpp>
|
||||
#include <boost/range/adaptor/map.hpp>
|
||||
#include <boost/range/adaptor/transformed.hpp>
|
||||
#include <boost/range/algorithm/find_if.hpp>
|
||||
#include <boost/range/algorithm/for_each.hpp>
|
||||
|
||||
#include <range/v3/algorithm/find_if.hpp>
|
||||
#include <range/v3/algorithm/for_each.hpp>
|
||||
#include <range/v3/algorithm/transform.hpp>
|
||||
#include <range/v3/action/join.hpp>
|
||||
#include <range/v3/action/sort.hpp>
|
||||
#include <range/v3/action/transform.hpp>
|
||||
#include <range/v3/iterator/insert_iterators.hpp>
|
||||
#include <range/v3/view/filter.hpp>
|
||||
#include <range/v3/view/join.hpp>
|
||||
#include <range/v3/view/iota.hpp>
|
||||
#include <range/v3/view/intersperse.hpp>
|
||||
#include <range/v3/view/map.hpp>
|
||||
#include <range/v3/view/zip.hpp>
|
||||
#include <range/v3/distance.hpp>
|
||||
#include <range/v3/range_for.hpp>
|
||||
|
||||
#include <boost/spirit/home/x3.hpp>
|
||||
|
||||
namespace HLE {
|
||||
|
||||
namespace OS {
|
||||
|
||||
ConsoleModule::ConsoleModule(OS& os) : os(os) {
|
||||
|
||||
}
|
||||
|
||||
template<typename... T>
|
||||
decltype(auto) LiftIfVariable(std::tuple<T...>&& tuple) {
|
||||
return std::forward(tuple);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
auto LiftIfVariable(T&& var) {
|
||||
return std::make_tuple(var);
|
||||
}
|
||||
|
||||
std::optional<std::string> ConsoleModule::HandleCommand(const std::string& command) {
|
||||
namespace x3 = boost::spirit::x3;
|
||||
|
||||
// Separator
|
||||
auto sep = x3::omit[+x3::blank];
|
||||
|
||||
auto process_id = x3::uint_parser<uint32_t, 10>();
|
||||
auto thread_id = x3::uint_parser<uint32_t, 10>();
|
||||
auto handle = process_id >> x3::lit(".") >> x3::uint_parser<uint32_t, 10>();
|
||||
auto object_pointer = x3::lit("0x") > x3::uint_parser<ptrdiff_t, 16>();
|
||||
|
||||
std::string ret;
|
||||
|
||||
// TODO: Use the one from repl.hpp instead!!!!
|
||||
auto wrap_handler = [](auto&& callee) {
|
||||
return [&callee](auto&& ctx) {
|
||||
namespace hana = boost::hana;
|
||||
auto args = hana::to_tuple(/*LiftIfVariable(*/x3::_attr(ctx)/*)*/);
|
||||
hana::unpack(args, callee);
|
||||
};
|
||||
};
|
||||
|
||||
auto wrap_singleton_handler = [](auto&& callee) {
|
||||
return [&callee](auto&& ctx) {
|
||||
callee(x3::_attr(ctx));
|
||||
};
|
||||
};
|
||||
|
||||
auto&& thread_info = [](Thread& thread) {
|
||||
std::string ret;
|
||||
auto&& process = thread.GetParentProcess();
|
||||
ret += fmt::format("Thread {}.{}, {}/{}: ", process.GetId(), thread.GetId(),
|
||||
process.GetName(), thread.GetName());
|
||||
using Status = HLE::OS::Thread::Status;
|
||||
switch (thread.status) {
|
||||
case Status::Ready:
|
||||
ret += "Ready";
|
||||
break;
|
||||
|
||||
case Status::Sleeping:
|
||||
{
|
||||
if (thread.wait_for_all)
|
||||
ret += "Waiting for all of [";
|
||||
else
|
||||
ret += "Waiting for any of [";
|
||||
|
||||
bool first = true;
|
||||
for (auto object : thread.wait_list) {
|
||||
if (object->GetName() == "CSession_Port_srv:") {
|
||||
auto service_name = Platform::SM::PortName::IPCDeserialize(thread.ReadTLS(0x84), thread.ReadTLS(0x88), 8).ToString();
|
||||
ret += fmt::format("{}(Service {} to be up)", first ? "" : ", ", service_name);
|
||||
} else {
|
||||
ret += fmt::format("{}{:#x} ({})", first ? "" : ", ", reinterpret_cast<uintptr_t>(object.get()), object->GetName());
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
ret += fmt::format("] with timeout={:#x}", thread.timeout_at);
|
||||
break;
|
||||
}
|
||||
|
||||
case Status::WaitingForTimeout:
|
||||
ret += "WaitingForTimeout in " + std::to_string((thread.timeout_at - process.GetOS().GetTimeInNanoSeconds()) / 1000000.f) + " ms";
|
||||
break;
|
||||
|
||||
case Status::WaitingForArbitration:
|
||||
ret += fmt::format("Arbiting on address {:#010x} via arbiter {}", thread.arbitration_address,
|
||||
process.handle_table.FindObject<AddressArbiter>(thread.arbitration_handle)->GetName());
|
||||
break;
|
||||
|
||||
case Status::Stopped:
|
||||
ret += "Stopped";
|
||||
break;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
auto&& handle_debug_info = [&](const DebugHandle::DebugInfo& handle_info, Object* object_ptr) {
|
||||
std::string ret = fmt::format("created after {:#x} ticks", handle_info.created_time_stamp);
|
||||
switch (handle_info.source) {
|
||||
case DebugHandle::Original:
|
||||
break;
|
||||
|
||||
case DebugHandle::Duplicated:
|
||||
ret += ", duplicate of TODO";
|
||||
break;
|
||||
|
||||
case DebugHandle::FromIPC:
|
||||
ret += fmt::format(", copied via IPC command {:#010x}", handle_info.ipc_command_header);
|
||||
break;
|
||||
|
||||
default:
|
||||
ret += ", unknown origin";
|
||||
break;
|
||||
}
|
||||
|
||||
auto&& is_given_object = [&](std::weak_ptr<Event> event) {
|
||||
return object_ptr && object_ptr == event.lock().get();
|
||||
};
|
||||
|
||||
// Gather list of interrupts this object may be subscribed to
|
||||
std::vector<uint32_t> interrupt_subscriptions;
|
||||
// TODO: Instead of this zip, we can just use ranges::enumerate!
|
||||
ranges::for_each(ranges::view::zip(ranges::view::ints, os.bound_interrupts),
|
||||
[&](auto&& interrupt_subscribers_pair) {
|
||||
auto&& replace_with_interrupt_id = [&](auto&&) { return interrupt_subscribers_pair.first; };
|
||||
auto&& subscriber_list = interrupt_subscribers_pair.second;
|
||||
ranges::transform(subscriber_list | ranges::view::filter(is_given_object),
|
||||
ranges::back_inserter(interrupt_subscriptions),
|
||||
replace_with_interrupt_id);
|
||||
});
|
||||
if (!interrupt_subscriptions.empty()) {
|
||||
ret += fmt::format(", subscribed to interrupt{} ", interrupt_subscriptions.size() == 1 ? "" : "s");
|
||||
auto&& to_hex_string = [](uint32_t interrupt) { return fmt::format("{:#x}", interrupt); };
|
||||
ranges::for_each(interrupt_subscriptions | ranges::view::transform(to_hex_string) | ranges::view::intersperse(", "),
|
||||
[&](auto&& new_string) { ret += new_string; });
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
auto&& handle_info = [this,handle_debug_info](const DebugHandle& handle, Object* object_ptr) {
|
||||
auto&& handle_info = handle.debug_info;
|
||||
auto ret = fmt::format("Handle {} ({}), ", handle.value, handle_debug_info(handle_info, object_ptr));
|
||||
if (auto* timer = dynamic_cast<Timer*>(object_ptr)) {
|
||||
ret += fmt::format("{}, ", timer->type == ResetType::OneShot ? "one shot"
|
||||
: timer->type == ResetType::Sticky ? "sticky"
|
||||
: timer->type == ResetType::Pulse ? "pulse"
|
||||
: "other");
|
||||
ret += fmt::format("{}, ", timer->active ? "active" : "inactive");
|
||||
auto timeout_in = timer->timeout_time_ns - os.GetTimeInNanoSeconds();
|
||||
ret += fmt::format("timeout in {}.{:03}, ", timeout_in / 1000, timeout_in % 1000);
|
||||
ret += fmt::format("period {}.{:03}, ", timer->period_ns / 1000, timer->period_ns % 1000);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
auto about_memory = [&]() {
|
||||
std::string ret;
|
||||
for (size_t manager_idx = 0; manager_idx < os.memory_regions.size(); ++manager_idx) {
|
||||
auto& manager = os.memory_regions[manager_idx];
|
||||
for (auto& mapping : manager.taken) {
|
||||
auto owner = mapping.second.owner.lock();
|
||||
ret += fmt::format("[{:#010x}-{:#010x}] ({:#x} bytes, owned by {} {})\n",
|
||||
mapping.first, mapping.first + mapping.second.size_bytes, mapping.second.size_bytes,
|
||||
owner ? typeid(*owner).name() : "", fmt::ptr(owner.get()));
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
auto&& about_thread = [&](ProcessId pid, ThreadId tid) {
|
||||
auto&& process = os.GetProcessFromId(pid);
|
||||
if (!process) {
|
||||
ret += "Unknown process ID\n";
|
||||
return;
|
||||
}
|
||||
auto&& thread = process->GetThreadFromId(tid);
|
||||
if (!thread) {
|
||||
ret += "Unknown thread ID\n";
|
||||
return;
|
||||
}
|
||||
|
||||
ret += thread_info(*thread);
|
||||
// TODO: Do the following only for EmuThreads!
|
||||
ret += "\n\nCPU registers:\n";
|
||||
std::array<const char*,17> reg_names = {{
|
||||
"r0 ", "r1 ", "r2 ", "r3 ", "r4 ", "r5 ", "r6 ", "r7 ",
|
||||
"r8 ", "r9 ", "r10 ", "r11 ", "r12 ", "r13 ", "r14 ", "r15 ",
|
||||
"cpsr"
|
||||
}};
|
||||
RANGES_FOR (auto&& name_and_index, ranges::view::zip(reg_names, ranges::view::ints)) {
|
||||
ret += fmt::format("{} {:#x}\n", name_and_index.first, thread->GetCPURegisterValue(name_and_index.second));
|
||||
}
|
||||
|
||||
// TODO: Print stack trace
|
||||
};
|
||||
|
||||
auto&& about_process = [&](ProcessId pid) {
|
||||
auto process = os.process_handles[pid];
|
||||
ret += "Process " + process->GetName() + "\n\n";
|
||||
|
||||
ret += "\nMemory map:\n";
|
||||
decltype(process->virtual_memory) contiguous_regions;
|
||||
auto memory_region_it = process->virtual_memory.begin();
|
||||
while (memory_region_it != std::end(process->virtual_memory)) {
|
||||
auto is_noncontiguous = [](auto left, auto right) {
|
||||
auto left_vaddr_end = left.first + left.second.size;
|
||||
auto right_vaddr_start = right.first;
|
||||
auto left_paddr_end = left.second.phys_start + left.second.size;
|
||||
auto right_paddr_start = right.second.phys_start;
|
||||
auto left_permissions = left.second.permissions;
|
||||
auto right_permissions = right.second.permissions;
|
||||
|
||||
return (left_vaddr_end != right_vaddr_start) ||
|
||||
(left_paddr_end != right_paddr_start) ||
|
||||
(left_permissions != right_permissions);
|
||||
};
|
||||
|
||||
// Find the last contiguous map entry for the current region
|
||||
// (if no noncontiguity can be found, the current region extends until the last map entry)
|
||||
auto end_region_it = std::adjacent_find(memory_region_it, std::end(process->virtual_memory),
|
||||
is_noncontiguous);
|
||||
if (end_region_it == std::end(process->virtual_memory))
|
||||
end_region_it = std::prev(end_region_it);
|
||||
|
||||
auto region_start_virt = memory_region_it->first;
|
||||
auto region_start_phys = memory_region_it->second.phys_start;
|
||||
auto region_size = end_region_it->first + end_region_it->second.size - region_start_virt;
|
||||
auto raw_permissions = Meta::to_underlying(memory_region_it->second.permissions);
|
||||
ret += fmt::format("[{:#010x}-{:#010x}] -> [{:#010x}-{:#010x}] ({:#x} bytes, {}{}{})\n",
|
||||
region_start_virt, region_start_virt + region_size,
|
||||
region_start_phys, region_start_phys + region_size,
|
||||
region_size, (raw_permissions & 1) ? "R" : "", (raw_permissions & 2) ? "W" : "", (raw_permissions & 4) ? "X" : "");
|
||||
|
||||
memory_region_it = std::next(end_region_it);
|
||||
}
|
||||
|
||||
ret += "\nHandle table:\n";
|
||||
std::map<DebugHandle, std::shared_ptr<Object>, std::less<Handle>> sorted_handle_table;
|
||||
ranges::copy(process->handle_table.table, ranges::inserter(sorted_handle_table, ranges::begin(sorted_handle_table)));
|
||||
for (auto entry : sorted_handle_table) {
|
||||
ret += fmt::format("Handle {} -> {:#x} ({})", entry.first.value, reinterpret_cast<uintptr_t>(entry.second.get()), entry.second ? entry.second->GetName() : "INVALID");
|
||||
ret += ": " + handle_debug_info(entry.first.debug_info, entry.second.get()) + "\n";
|
||||
}
|
||||
|
||||
ret += "\nThreads:\n";
|
||||
std::vector<std::shared_ptr<Thread>> sorted_threads;
|
||||
ranges::copy(process->threads, ranges::back_inserter(sorted_threads));
|
||||
ranges::sort(sorted_threads, [](auto&& thread1, auto&& thread2) { return thread1->GetId() < thread2->GetId(); });
|
||||
for (auto thread : sorted_threads) {
|
||||
ret += thread_info(*thread);
|
||||
ret += '\n';
|
||||
}
|
||||
};
|
||||
|
||||
auto about_object = [&](uintptr_t object) {
|
||||
// Safely find an Object reference to the given object address by searching all handle tables for the object
|
||||
std::map<ProcessId, DebugHandle> matching_handles;
|
||||
|
||||
auto&& get_handle = [=](auto&& process) {
|
||||
return process->handle_table.FindHandle(reinterpret_cast<void*>(object));
|
||||
};
|
||||
|
||||
auto&& handle_is_present = [=](auto&& process) {
|
||||
return Handle{HANDLE_INVALID} != get_handle(process);
|
||||
};
|
||||
|
||||
auto&& get_tagged_handle = [=](auto&& process) {
|
||||
return std::make_pair(process->GetId(), get_handle(process));
|
||||
};
|
||||
|
||||
ranges::transform(os.process_handles | ranges::view::values | ranges::view::filter(handle_is_present), ranges::inserter(matching_handles, ranges::begin(matching_handles)),
|
||||
get_tagged_handle);
|
||||
if (ranges::empty(matching_handles)) {
|
||||
ret += fmt::format("No kernel object found at address {:#x}", object);
|
||||
} else {
|
||||
// Pick some process in the list to safely look up the actual Object pointer
|
||||
auto some_match = *matching_handles.begin();
|
||||
auto object_ptr = os.process_handles[some_match.first]->handle_table.FindObject<Object>(some_match.second);
|
||||
ret += fmt::format("Kernel object {:#x} ({}) is explicitly referenced in:\n", object, object_ptr->GetName());
|
||||
// TODO: Further information based on the dynamic object type!
|
||||
// TODO: List interrupts this object was subscribed to via SVCBindInterrupt
|
||||
|
||||
for (auto handles : matching_handles) {
|
||||
auto pid = handles.first;
|
||||
|
||||
ret += fmt::format("Process {} ({}): ", pid, os.process_handles[pid]->GetName());
|
||||
ret += handle_info(handles.second, object_ptr.get());
|
||||
ret += '\n';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
auto&& lookup_and_set_object_name = [&](ProcessId pid, uint32_t raw_handle, std::string name) {
|
||||
if (0 == os.process_handles.count(pid)) {
|
||||
ret += "Unknown process ID\n";
|
||||
return;
|
||||
}
|
||||
|
||||
auto&& process = os.process_handles[pid];
|
||||
auto&& object = process->handle_table.FindObject<Object>(Handle{raw_handle});
|
||||
if (object == nullptr) {
|
||||
ret += "Handle not found in process handle table\n";
|
||||
return;
|
||||
}
|
||||
|
||||
object->name = name;
|
||||
ret += "Ok\n";
|
||||
};
|
||||
|
||||
if (x3::parse(command.begin(), command.end(), x3::lit("about threads"))) {
|
||||
std::string ret;
|
||||
// TODO: Thread-safety!
|
||||
// TODO: Embed context information for EmuThreads!
|
||||
for (auto& pair : os.process_handles) {
|
||||
auto process = pair.second;
|
||||
|
||||
std::vector<std::shared_ptr<Thread>> sorted_threads;
|
||||
ranges::copy(process->threads, ranges::back_inserter(sorted_threads));
|
||||
ranges::sort(sorted_threads, [](auto&& thread1, auto&& thread2) { return thread1->GetId() < thread2->GetId(); });
|
||||
|
||||
for (auto thread : sorted_threads) {
|
||||
ret += thread_info(*thread);
|
||||
ret += '\n';
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
} else if (x3::parse(command.begin(), command.end(), x3::lit("about memory"))) {
|
||||
return about_memory();
|
||||
} else if (x3::parse(command.begin(), command.end(), (x3::lit("about thread") >> sep >> process_id >> x3::lit(".") >> thread_id)[wrap_handler(about_thread)])) {
|
||||
return ret;
|
||||
} else if (x3::parse(command.begin(), command.end(), (x3::lit("about process") >> sep >> process_id)[wrap_singleton_handler(about_process)])) {
|
||||
return ret;
|
||||
} else if (x3::parse(command.begin(), command.end(), x3::lit("about") >> sep >> object_pointer[wrap_singleton_handler(about_object)])) {
|
||||
return ret;
|
||||
} else if (x3::parse(command.begin(), command.end(), x3::lit("about objects") >> +(sep >> object_pointer[wrap_singleton_handler(about_object)]))) {
|
||||
return ret;
|
||||
} else if (x3::parse(command.begin(), command.end(), x3::lit("set name") >> sep >> (handle >> sep >> +x3::graph)[wrap_handler(lookup_and_set_object_name)])) {
|
||||
return ret;
|
||||
} else if (x3::parse(command.begin(), command.end(), x3::lit("help"))) {
|
||||
return {{"Commands:\nabout threads\nabout process <pid>\nabout <host object address>\nset name <pid>.<handle> <name>"}};
|
||||
} else {
|
||||
return {{"Unknown command!"}};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace OS
|
||||
|
||||
} // namespace HLE
|
23
source/os_console.hpp
Normal file
23
source/os_console.hpp
Normal file
|
@ -0,0 +1,23 @@
|
|||
#pragma once
|
||||
|
||||
#include "framework/console.hpp"
|
||||
|
||||
namespace HLE {
|
||||
|
||||
namespace OS {
|
||||
|
||||
class OS;
|
||||
|
||||
class ConsoleModule : public ::ConsoleModule {
|
||||
OS& os;
|
||||
|
||||
public:
|
||||
ConsoleModule(OS& os);
|
||||
virtual ~ConsoleModule() = default;
|
||||
|
||||
virtual std::optional<std::string> HandleCommand(const std::string& command) override;
|
||||
};
|
||||
|
||||
} // namespace OS
|
||||
|
||||
} // namespace HLE
|
373
source/os_hypervisor.cpp
Normal file
373
source/os_hypervisor.cpp
Normal file
|
@ -0,0 +1,373 @@
|
|||
#include "os.hpp"
|
||||
#include "os_hypervisor.hpp"
|
||||
#include "os_hypervisor_private.hpp"
|
||||
|
||||
#include "processes/am_hpv.hpp"
|
||||
#include "processes/dsp_hpv.hpp"
|
||||
#include "processes/fs_hpv.hpp"
|
||||
#include "processes/ns_hpv.hpp"
|
||||
#include "processes/sm_hpv.hpp"
|
||||
#include "processes/ro_hpv.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace HLE {
|
||||
|
||||
namespace OS {
|
||||
|
||||
namespace HPV {
|
||||
|
||||
void Session::OnRequest(Hypervisor& hv, Thread& from_thread, Thread& to_thread, Handle session) {
|
||||
client_thread = &from_thread;
|
||||
OnRequest(hv, to_thread, session);
|
||||
client_thread = nullptr;
|
||||
}
|
||||
|
||||
void Session::OnRequest(Hypervisor& hv, Thread& to_thread, Handle session) {
|
||||
OnRequest(hv, to_thread, session, fmt::format("{:08x}", to_thread.ReadTLS(0x80)));
|
||||
}
|
||||
|
||||
void Session::OnRequest(Hypervisor&, Thread& thread, Handle, std::string_view command_description) {
|
||||
thread.GetLogger()->info("IPC message from {} to {}: {}", client_thread->GetParentProcess().GetName(), Describe(), command_description);
|
||||
}
|
||||
|
||||
// Empty context for unrecognized sessions
|
||||
struct NullContext : SessionContext {
|
||||
};
|
||||
|
||||
struct State {
|
||||
using HandleTable = std::unordered_map<Handle, HPV::RefCounted<HPV::Object>>;
|
||||
|
||||
std::unordered_map<ProcessId, HandleTable> handle_tables;
|
||||
|
||||
// TODO: Not actually needed anymore.
|
||||
std::vector<HPV::RefCounted<HPV::Port>> ports;
|
||||
|
||||
HPV::NullContext null_context;
|
||||
HPV::AMContext am_context;
|
||||
HPV::DSPContext dsp_context;
|
||||
HPV::FSContext fs_context;
|
||||
HPV::SMContext sm_context;
|
||||
HPV::NSContext ns_context;
|
||||
HPV::ROContext ro_context;
|
||||
|
||||
template<typename T>
|
||||
HPV::RefCounted<T> FindObject(ProcessId process, Handle handle) const {
|
||||
static_assert(std::is_base_of_v<HPV::Object, T>, "Given object type is not derived from HPV::Object");
|
||||
|
||||
auto handle_table_it = handle_tables.find(process);
|
||||
if (handle_table_it == handle_tables.end()) {
|
||||
throw std::runtime_error("Precondition violated: No handles are registered for this process");
|
||||
}
|
||||
auto& handle_table = handle_table_it->second;
|
||||
|
||||
auto object_it = handle_table.find(handle);
|
||||
if (object_it == handle_table.end()) {
|
||||
throw std::runtime_error(fmt::format("Precondition violated: Handle {:#x} is not registered", handle.value));
|
||||
}
|
||||
|
||||
auto typed_object = HPV::dynamic_refcounted_cast<T>(object_it->second);
|
||||
if (!typed_object) {
|
||||
throw std::runtime_error("Precondition violated: Given handle is registered to an object that does not represent the given type");
|
||||
}
|
||||
|
||||
return typed_object;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Hypervisor::Hypervisor() : state(new HPV::State) {
|
||||
}
|
||||
|
||||
Hypervisor::~Hypervisor() = default;
|
||||
|
||||
void Hypervisor::OnPortCreated(ProcessId process, std::string_view port_name, Handle port_handle) {
|
||||
auto& handle_table = state->handle_tables[process];
|
||||
if (handle_table.count(port_handle)) {
|
||||
throw std::runtime_error("Precondition violated: A Port for this handle is already registered");
|
||||
}
|
||||
|
||||
auto port = HPV::RefCounted(new HPV::NamedPort(port_name));
|
||||
state->ports.push_back(HPV::static_refcounted_cast<HPV::Port>(port));
|
||||
handle_table.emplace(port_handle, HPV::static_refcounted_cast<HPV::Object>(port));
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* Adapts service factories like CreateSMSrvService (taking SessionContext& as
|
||||
* second parameter) to a function taking an HPV::State instead. The required
|
||||
* SessionContext is looked up using the template parameter "Context"
|
||||
*/
|
||||
template<auto F, auto Context>
|
||||
HPV::RefCounted<HPV::Object> WrapSessionFactory(HPV::RefCounted<HPV::Port> port, HPV::State& state) {
|
||||
return F(port, state.*Context);
|
||||
}
|
||||
|
||||
using SessionFactoryType = std::add_pointer_t<HPV::RefCounted<HPV::Object>(HPV::RefCounted<HPV::Port>, HPV::State&)>;
|
||||
using namespace std::string_view_literals;
|
||||
std::unordered_map<std::string_view, SessionFactoryType> service_factory_map = {{
|
||||
{ "am:app"sv, WrapSessionFactory<HPV::CreateAmService, &HPV::State::am_context> },
|
||||
{ "am:net"sv, WrapSessionFactory<HPV::CreateAmService, &HPV::State::am_context> },
|
||||
{ "am:sys"sv, WrapSessionFactory<HPV::CreateAmService, &HPV::State::am_context> },
|
||||
{ "am:u"sv, WrapSessionFactory<HPV::CreateAmService, &HPV::State::am_context> },
|
||||
|
||||
{ "dsp::DSP"sv, WrapSessionFactory<HPV::CreateDspService, &HPV::State::dsp_context> },
|
||||
|
||||
{ "fs:USER"sv, WrapSessionFactory<HPV::CreateFSUserService, &HPV::State::fs_context> },
|
||||
{ "fs:LDR"sv, WrapSessionFactory<HPV::CreateFSLdrService, &HPV::State::fs_context> },
|
||||
{ "fs:REG"sv, WrapSessionFactory<HPV::CreateFSRegService, &HPV::State::fs_context> },
|
||||
{ "srv:"sv, WrapSessionFactory<HPV::CreateSMSrvService, &HPV::State::sm_context> },
|
||||
{ "srv:pm"sv, WrapSessionFactory<HPV::CreateSMSrvPmService, &HPV::State::sm_context> },
|
||||
{ "ns:s"sv, WrapSessionFactory<HPV::CreateNSService, &HPV::State::ns_context> },
|
||||
{ "APT:S"sv, WrapSessionFactory<HPV::CreateAPTService, &HPV::State::ns_context> },
|
||||
|
||||
{ "ldr:ro"sv, WrapSessionFactory<HPV::CreateRoService, &HPV::State::ro_context> },
|
||||
}};
|
||||
|
||||
HPV::RefCounted<HPV::Object> SessionFactory(HPV::State& state, HPV::RefCounted<HPV::Port> port) {
|
||||
auto name = port->Name();
|
||||
|
||||
auto factory = service_factory_map.find(name);
|
||||
if (factory == service_factory_map.end()) {
|
||||
// Unrecognized service. Return default Session
|
||||
return HPV::RefCounted<HPV::Object>(new HPV::SessionToPort(port, state.null_context));
|
||||
}
|
||||
return (factory->second)(port, state);
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
void Hypervisor::OnConnectToPort(ProcessId process, std::string_view port_name, Handle session_handle) {
|
||||
auto& handle_table = state->handle_tables[process];
|
||||
if (handle_table.count(session_handle)) {
|
||||
throw std::runtime_error("Precondition violated: A Session for this handle is already registered");
|
||||
}
|
||||
|
||||
auto port_it = std::find_if(state->ports.begin(), state->ports.end(), [=](auto& port) { return (port->Name() == port_name); });
|
||||
if (port_it == state->ports.end()) {
|
||||
throw std::runtime_error("No port with the name \"" + std::string { port_name } + "\" exists");
|
||||
}
|
||||
|
||||
handle_table.emplace(session_handle, SessionFactory(*state, *port_it));
|
||||
}
|
||||
|
||||
void Hypervisor::OnNewSession(ProcessId process, Handle port_handle, Handle session_handle) {
|
||||
auto& handle_table = state->handle_tables[process];
|
||||
if (handle_table.count(session_handle)) {
|
||||
throw std::runtime_error("Precondition violated: A Session for this handle is already registered");
|
||||
}
|
||||
|
||||
auto port = state->FindObject<HPV::Port>(process, port_handle);
|
||||
|
||||
handle_table.emplace(session_handle, SessionFactory(*state, port));
|
||||
}
|
||||
|
||||
void Hypervisor::OnSessionCreated(ProcessId process, Handle session_handle) {
|
||||
auto& handle_table = state->handle_tables[process];
|
||||
if (handle_table.count(session_handle)) {
|
||||
throw std::runtime_error("Precondition violated: A Session for this handle is already registered");
|
||||
}
|
||||
|
||||
// This is a placeholder Session: If there is other HPV that names it
|
||||
// (usually the response handler for the IPC command that caused this
|
||||
// session to be created, e.g. SM::SRV::RegisterService), we'll replace
|
||||
// it with a more specific type
|
||||
auto session = HPV::RefCounted(new HPV::UnnamedSession);
|
||||
handle_table.emplace(session_handle, HPV::static_refcounted_cast<HPV::Object>(session));
|
||||
}
|
||||
|
||||
void Hypervisor::SetSessionObject(ProcessId process, Handle session_handle, HPV::Session* object) {
|
||||
auto session = state->FindObject<HPV::Session>(process, session_handle);
|
||||
session.ReplaceManagedObject(object);
|
||||
}
|
||||
|
||||
void Hypervisor::SetPortName(ProcessId process, Handle port_handle, std::string_view port_name) {
|
||||
auto port = state->FindObject<HPV::Port>(process, port_handle);
|
||||
port.ReplaceManagedObject(new HPV::ServicePort(port_name));
|
||||
}
|
||||
|
||||
void Hypervisor::OnHandleDuplicated(ProcessId source_process, Handle source_handle, ProcessId dest_process, Handle dest_handle) {
|
||||
auto& source_handle_table = state->handle_tables[source_process];
|
||||
auto session_it = source_handle_table.find(source_handle);
|
||||
if (session_it == source_handle_table.end()) {
|
||||
// We don't track all kinds of handles yet, so the given handle may not
|
||||
// have made it into our handle table in the first place.
|
||||
// Hence, this is not an error.
|
||||
return;
|
||||
}
|
||||
|
||||
auto& dest_handle_table = state->handle_tables[dest_process];
|
||||
if (dest_handle_table.count(dest_handle)) {
|
||||
throw std::runtime_error("Precondition violated: A Session for this handle is already registered");
|
||||
}
|
||||
dest_handle_table.emplace(dest_handle, session_it->second);
|
||||
}
|
||||
|
||||
void Hypervisor::OnHandleClosed(ProcessId process, Handle handle) {
|
||||
state->handle_tables[process].erase(handle);
|
||||
}
|
||||
|
||||
void Hypervisor::OnIPCRequestFromTo(Thread& from_thread, Thread& to_thread, Handle session_handle) {
|
||||
auto session = state->FindObject<HPV::Session>(to_thread.GetParentProcess().GetId(), session_handle);
|
||||
|
||||
// TODO: Verify command header against expectation
|
||||
|
||||
session->OnRequest(*this, from_thread, to_thread, session_handle);
|
||||
}
|
||||
|
||||
// TODO: Need to have the reply target argument back
|
||||
void Hypervisor::OnIPCReplyFromTo(Thread& from_thread, Thread& to_thread, Handle session_handle) {
|
||||
auto session = state->FindObject<HPV::Session>(from_thread.GetParentProcess().GetId(), session_handle);
|
||||
|
||||
if (session->hpv_ipc_callback) // TODO: Instead, put the callback into a well-defined state all the time!
|
||||
session->hpv_ipc_callback(*this, from_thread);
|
||||
session->hpv_ipc_callback = [](Hypervisor&, Thread&) {};
|
||||
|
||||
// TODO: Verify response header against expectation
|
||||
}
|
||||
|
||||
// TODO: Drop Thread parameter. Instead, add memory access interface for hypervisor
|
||||
void Hypervisor::OnEventSignaled(Thread& thread, ProcessId process, Handle event_handle) {
|
||||
// // TODO: Should keep a list of shadow events that shadow services subscribe to
|
||||
|
||||
// // For now, we just hardcode some DSP code
|
||||
// if (process != 17 || thread.GetProcessHandleTable().FindObject<Event>(event_handle)->GetName() != "DSPSemaphoreEvent") {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Look for DSP-side handle...
|
||||
// process = 25;
|
||||
// event_handle.value = 8;
|
||||
|
||||
// for (auto& entry : state->handle_tables.at(process)) {
|
||||
// auto obj = dynamic_refcounted_cast<HPV::SessionToPort>(entry.second);
|
||||
// if (obj && obj->Describe() == "dsp::DSP") {
|
||||
// [[deprecated]] void OnDSPSemaphoreEventSignaled(Thread&, HPV::RefCounted<HPV::SessionToPort>&);
|
||||
// OnDSPSemaphoreEventSignaled(thread, obj);
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
// throw std::runtime_error("Could not find DSP port");
|
||||
}
|
||||
|
||||
|
||||
void Hypervisor::SetObjectTag(Thread& thread, Handle handle, std::string name) {
|
||||
auto object = thread.GetProcessHandleTable().FindObject<Object>(handle);
|
||||
assert(object);
|
||||
object->name = std::move(name);
|
||||
}
|
||||
|
||||
#if 0
|
||||
/**
|
||||
* todo.
|
||||
*
|
||||
* Gets the thread that sent the message. Note this function gets called before the IPC message is marshalled, so mixing up the threads will get us the wrong arguments!
|
||||
*/
|
||||
template<typename CommandParameterList, typename Func, typename Thread, typename... ExtraArgs>
|
||||
static auto HandleIntrospection(Func handler, Thread& thread, ExtraArgs&&... args) {
|
||||
using TypeList = boost::mp11::mp_rename<CommandParameterList, std::tuple>;
|
||||
|
||||
// if (thread.ReadTLS(0x80) != Command::request_header)
|
||||
// throw IPCError{thread.ReadTLS(0x80), 0xdeadbeef};
|
||||
//
|
||||
// Read request data from TLS
|
||||
// TODO: Actually, we would prefer some sort of generate() algorithm instead of having to instantiate the TypeList for transformation.
|
||||
auto input_data = TransformTupleSequentially(IPC::TLSReader(thread), TypeList{});
|
||||
|
||||
// Invoke request handler
|
||||
// TODO: Can we statically_assert the number of input parameters here?
|
||||
// Note that the input function may be a lambda expression, which may
|
||||
// be harder to introspect!
|
||||
// TODO: The following may perform implicit conversion of uint64_t
|
||||
// parameters to uint32_t (or vice versa), hence it actually provides
|
||||
// less compile-time safety that we would like it to, currently.
|
||||
// NOTE: bound_handler was initialized using hana::partial before, but
|
||||
// that unfortunately captures variables by value (i.e. imperfectly),
|
||||
// hence making it useless when trying to forward reference
|
||||
// parameters.
|
||||
auto bound_handler = [&handler,&args...](auto&&... inputs) { return handler(std::forward<ExtraArgs>(args)..., inputs...); };
|
||||
return std::apply(bound_handler, input_data);
|
||||
}
|
||||
|
||||
static std::unique_ptr<IPCMessage> APTInitialize(Thread& server, uint32_t app_id, uint32_t applet_attr) {
|
||||
server.GetLogger()->info("{}received Initialize with AppID={:#x}, AppletAttr={:#x}",
|
||||
ThreadPrinter{server}, app_id, applet_attr);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::unique_ptr<IPCMessage> Hypervisor::IntrospectIPCRequest(Thread& source, Thread& dest) {
|
||||
switch (dest.GetParentProcess().GetId()) {
|
||||
default:
|
||||
if (ProcessHasName(dest, "ns")) {
|
||||
namespace APT = Platform::NS::APT;
|
||||
if (source.ReadTLS(0x80) == APT::Initialize::request_header) {
|
||||
return HandleIntrospection<APT::Initialize::request_list>(APTInitialize, source, dest);
|
||||
} else {
|
||||
dest.GetLogger()->info("{}unrecognized IPC command with header {:#010x}",
|
||||
ThreadPrinter{dest}, source.ReadTLS(0x80));
|
||||
return nullptr;
|
||||
}
|
||||
} else if (ProcessHasName(dest, "gsp") || ProcessHasName(dest, "FakeGSP")) {
|
||||
namespace GSPGPU = Platform::GSP::GPU;
|
||||
if (source.ReadTLS(0x80) == GSPGPU::RegisterInterruptRelayQueue::request_header) {
|
||||
dest.GetLogger()->info("{}RegisterInterruptRelayQueue",
|
||||
ThreadPrinter{dest});
|
||||
auto interrupt_event = source.GetProcessHandleTable().FindObject<Event>({source.ReadTLS(0x8c)});
|
||||
assert(interrupt_event);
|
||||
interrupt_event->name = "GSPInterruptQueueEvent";
|
||||
return nullptr;
|
||||
} else {
|
||||
dest.GetLogger()->info("{}unrecognized IPC command with header {:#010x}",
|
||||
ThreadPrinter{dest}, source.ReadTLS(0x80));
|
||||
return nullptr;
|
||||
}
|
||||
} else {
|
||||
dest.GetLogger()->info("{}OSHPV: unrecognized IPC command with header {:#010x}, {}",
|
||||
ThreadPrinter{dest}, source.ReadTLS(0x80), dest.GetParentProcess().GetName());
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void Hypervisor::IntrospectIPCReply(Thread& server, Thread& client, std::unique_ptr<HVIPCMessageTracker> request_data) {
|
||||
if (ProcessHasName(server, "ns")) {
|
||||
server.GetLogger()->info("bllaaaa {:#x}", client.ReadTLS(0x80));
|
||||
if (client.ReadTLS(0x80) == Platform::NS::APT::Initialize::request_header) {
|
||||
assert(server.ReadTLS(0x80) == Platform::NS::APT::Initialize::response_header);
|
||||
|
||||
auto notification_event = server.GetProcessHandleTable().FindObject<Event>({server.ReadTLS(0x8c)});
|
||||
assert(notification_event);
|
||||
notification_event->name = "APTNotificationEvent";
|
||||
|
||||
auto resume_event = server.GetProcessHandleTable().FindObject<Event>({server.ReadTLS(0x90)});
|
||||
assert(resume_event);
|
||||
resume_event->name = "APTResumeEvent";
|
||||
/*} else if (server.ReadTLS(0x80) == 0x00020043 && ProcessHasName(client, "menu")) {
|
||||
auto notification_event = server.GetProcessHandleTable().FindObject<Event>({server.ReadTLS(0x8c)});
|
||||
assert(notification_event);
|
||||
notification_event->name = "APTNotificationEvent";
|
||||
|
||||
auto resume_event = server.GetProcessHandleTable().FindObject<Event>({server.ReadTLS(0x90)});
|
||||
assert(resume_event);
|
||||
resume_event->name = "APTResumeEvent"; */
|
||||
} else if (server.ReadTLS(0x80) == 0x000100c2 && ProcessHasName(client, "menu")) {
|
||||
auto apt_lock = server.GetProcessHandleTable().FindObject<Mutex>({server.ReadTLS(0x94)});
|
||||
assert(apt_lock);
|
||||
apt_lock->name = "APTLock";
|
||||
}
|
||||
} else if (ProcessHasName(server, "mcu")) {
|
||||
if (server.ReadTLS(0x80) == 0x000d0042 && ProcessHasName(client, "gsp")) {
|
||||
auto event = server.GetProcessHandleTable().FindObject<Event>({server.ReadTLS(0x8c)});
|
||||
assert(event);
|
||||
event->name = "MCU_GPUEvent";
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace OS
|
||||
|
||||
} // namespace HLE
|
78
source/os_hypervisor.hpp
Normal file
78
source/os_hypervisor.hpp
Normal file
|
@ -0,0 +1,78 @@
|
|||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
|
||||
namespace HLE {
|
||||
|
||||
namespace OS {
|
||||
|
||||
class Thread;
|
||||
class Process;
|
||||
class CodeSet;
|
||||
struct Handle;
|
||||
|
||||
using ProcessId = uint32_t;
|
||||
|
||||
class Hypervisor;
|
||||
|
||||
namespace HPV {
|
||||
|
||||
class Session;
|
||||
|
||||
// TODO: Small-function optimization!
|
||||
using IPCCallback = std::function<void(Hypervisor&, Thread&)>;
|
||||
|
||||
struct State;
|
||||
|
||||
} // namespace HPV
|
||||
|
||||
class Hypervisor {
|
||||
std::unique_ptr<HPV::State> state;
|
||||
|
||||
public:
|
||||
Hypervisor();
|
||||
~Hypervisor();
|
||||
|
||||
// "thread" refers to the target (i.e. server) thread
|
||||
void OnIPCRequestFromTo(Thread& from_thread, Thread &to_thread, Handle session_handle);
|
||||
|
||||
// "thread" refers to the source (i.e. server) thread
|
||||
void OnIPCReplyFromTo(Thread& from_thread, Thread &to_thread, Handle session_handle);
|
||||
|
||||
void OnPortCreated(ProcessId process, std::string_view port_name, Handle port_handle);
|
||||
|
||||
// Registers the session handle as associated to the given named port.
|
||||
// @note Sessions are associated to *unnamed* ports in ServiceManager::GetServiceHandle via OnSessionNamed
|
||||
void OnConnectToPort(ProcessId process, std::string_view port_name, Handle session_handle);
|
||||
|
||||
// Registers the session handle as associated to the given port
|
||||
void OnNewSession(ProcessId process, Handle port_handle, Handle session_handle);
|
||||
|
||||
// Registers a session handle that is not associated with any port
|
||||
void OnSessionCreated(ProcessId process, Handle session_handle);
|
||||
|
||||
// Will name a port indicated by the handle. Naming will propagate to all other handles to the same session
|
||||
void SetPortName(ProcessId process, Handle port_handle, std::string_view port_name);
|
||||
|
||||
// Takes ownership of the given Session
|
||||
void SetSessionObject(ProcessId process, Handle session_handle, HPV::Session*);
|
||||
|
||||
void OnHandleDuplicated(ProcessId source_process, Handle source_handle, ProcessId dest_process, Handle dest_handle);
|
||||
|
||||
void OnHandleClosed(ProcessId, Handle);
|
||||
|
||||
// TODO: Must be wired up internally for shadow services to subscribe to events
|
||||
void OnEventSignaled(Thread&, ProcessId, Handle event_handle);
|
||||
|
||||
/**
|
||||
* Associates the object referenced by the given handle in the given thread
|
||||
* with a human-readable name
|
||||
*/
|
||||
void SetObjectTag(Thread&, Handle, std::string name);
|
||||
};
|
||||
|
||||
} // namespace OS
|
||||
|
||||
} // namespace HLE
|
271
source/os_hypervisor_private.hpp
Normal file
271
source/os_hypervisor_private.hpp
Normal file
|
@ -0,0 +1,271 @@
|
|||
#pragma once
|
||||
|
||||
#include <os_types.hpp>
|
||||
|
||||
#include <ipc.hpp>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <boost/mp11/list.hpp>
|
||||
|
||||
namespace HLE {
|
||||
|
||||
namespace OS {
|
||||
|
||||
class Thread;
|
||||
|
||||
class Hypervisor;
|
||||
|
||||
namespace HPV {
|
||||
|
||||
struct ThreadLocalStorage;
|
||||
|
||||
struct Object {
|
||||
virtual ~Object() = default;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
struct RefCounted {
|
||||
RefCounted() = default;
|
||||
|
||||
RefCounted(T* raw_object) : object(new std::unique_ptr<Object>(static_cast<Object*>(raw_object))) {
|
||||
static_assert(std::is_base_of_v<HPV::Object, T>, "Given type is not a child class of HPV::Object");
|
||||
}
|
||||
|
||||
RefCounted(const RefCounted& oth) : object(oth.object) {
|
||||
}
|
||||
|
||||
RefCounted(RefCounted&& oth) : object(std::move(oth.object)) {
|
||||
}
|
||||
|
||||
RefCounted& operator=(const RefCounted& oth) = delete;
|
||||
RefCounted& operator=(RefCounted&& oth) = delete;
|
||||
|
||||
// Releases ownership of the previously managed object and takes ownership of the given one instead.
|
||||
// The internal reference count will not be changed.
|
||||
std::unique_ptr<Object> ReplaceManagedObject(T* new_object) {
|
||||
// return std::exchange(*object, new_object);
|
||||
auto old = std::move(*object);
|
||||
*object = std::unique_ptr<Object>(new_object);
|
||||
return old;
|
||||
}
|
||||
|
||||
operator bool() const {
|
||||
return object->get();
|
||||
}
|
||||
T* operator->() {
|
||||
return static_cast<T*>(object->get());
|
||||
}
|
||||
|
||||
const T* operator->() const {
|
||||
return static_cast<const T*>(object->get());
|
||||
}
|
||||
|
||||
std::shared_ptr<std::unique_ptr<Object>> object;
|
||||
};
|
||||
|
||||
template<typename T, typename U>
|
||||
static RefCounted<T> static_refcounted_cast(RefCounted<U> obj) {
|
||||
static_cast<void>(static_cast<T*>(obj.object->get())); // Throw away result, and just static cast to verify the involved pointers are compatible
|
||||
RefCounted<T> ret;
|
||||
ret.object = obj.object;
|
||||
return ret;
|
||||
}
|
||||
|
||||
template<typename T, typename U>
|
||||
static RefCounted<T> dynamic_refcounted_cast(RefCounted<U> obj) {
|
||||
if (!dynamic_cast<T*>(obj.object->get())) {
|
||||
return nullptr;
|
||||
}
|
||||
RefCounted<T> ret;
|
||||
ret.object = obj.object;
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context roughly equivalent to the state usually managed in actual HLE/LLE
|
||||
* services. Instances of this are shared between ports relate to the same
|
||||
* module (fs, sm, ...)
|
||||
*/
|
||||
struct SessionContext {
|
||||
protected:
|
||||
~SessionContext() = default;
|
||||
};
|
||||
|
||||
struct Port : Object {
|
||||
virtual std::string Name() const = 0;
|
||||
};
|
||||
|
||||
// Port accessible using a global identifier (assigned using SVCCreatePort)
|
||||
struct NamedPort : Port {
|
||||
NamedPort(std::string_view name) : name(name) {
|
||||
}
|
||||
|
||||
std::string Name() const final {
|
||||
return (name.empty() ? "UnknownPort" : name);
|
||||
}
|
||||
|
||||
std::string name;
|
||||
};
|
||||
|
||||
// Port accessible only through ServiceManager (name assigned using SM::SRV::RegisterService)
|
||||
struct ServicePort : Port {
|
||||
ServicePort(std::string_view name) : name(name) {
|
||||
}
|
||||
|
||||
std::string Name() const final {
|
||||
return (name.empty() ? "UnknownPort" : name);
|
||||
}
|
||||
|
||||
std::string name;
|
||||
};
|
||||
|
||||
struct Session : Object {
|
||||
virtual std::string Describe() const = 0;
|
||||
|
||||
void OnRequest(Hypervisor&, Thread&, Thread&, Handle session);
|
||||
|
||||
// Callback invoked when intercepting IPC replies on this session
|
||||
HPV::IPCCallback hpv_ipc_callback;
|
||||
|
||||
protected:
|
||||
// Default handler for the public OnRequest member function
|
||||
void OnRequest(Hypervisor&, Thread&, Handle session, std::string_view command_description);
|
||||
|
||||
virtual void OnRequest(Hypervisor&, Thread&, Handle session);
|
||||
|
||||
private:
|
||||
// Valid only during execution of OnRequest
|
||||
Thread* client_thread = nullptr;
|
||||
};
|
||||
|
||||
// Default implementation used as a placeholder until the Session is named. We replace the Session object in place with a more suiting subclass then.
|
||||
struct UnnamedSession : Session {
|
||||
// Placeholder for sessions that have no name associated to them, yet
|
||||
std::string Describe() const override {
|
||||
return "UnknownSession";
|
||||
}
|
||||
};
|
||||
|
||||
struct SessionToPort : Session {
|
||||
SessionToPort(RefCounted<Port> port, SessionContext& context) : port(port), context(context) {
|
||||
}
|
||||
|
||||
std::string Describe() const override {
|
||||
return port->Name();
|
||||
}
|
||||
|
||||
RefCounted<Port> port;
|
||||
SessionContext& context;
|
||||
};
|
||||
|
||||
struct NamedSession : Session {
|
||||
NamedSession(std::string_view name, SessionContext& context) : name(name), context(context) {
|
||||
}
|
||||
|
||||
std::string Describe() const override {
|
||||
return name;
|
||||
}
|
||||
|
||||
std::string name;
|
||||
SessionContext& context;
|
||||
};
|
||||
|
||||
template<typename Handler, typename... ExtraParameters, typename... Tags>
|
||||
static auto InvokeWithIPCArguments(Handler&& handler, IPC::TLSReader reader, boost::mp11::mp_list<Tags...>, ExtraParameters&&... extras) {
|
||||
constexpr bool handler_is_valid = std::is_invocable_v<Handler, ExtraParameters..., decltype(reader(Tags{}))...>;
|
||||
static_assert(handler_is_valid, "Given handler is not compatible with this IPC command");
|
||||
|
||||
// Hide the rest of this function behind a validity check to prevent compilers from spamming errors
|
||||
if constexpr (handler_is_valid) {
|
||||
using Result = decltype(handler(extras..., reader(Tags{})...));
|
||||
if constexpr (std::is_void_v<Result>) {
|
||||
Meta::CallWithSequentialEvaluation<Result> { std::forward<Handler>(handler), std::forward<ExtraParameters>(extras)..., reader(Tags{})... };
|
||||
} else {
|
||||
return Meta::CallWithSequentialEvaluation<Result> { std::forward<Handler>(handler), std::forward<ExtraParameters>(extras)..., reader(Tags{})... }.GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template<typename Result = void>
|
||||
struct DispatcherBase {
|
||||
Thread& thread;
|
||||
uint32_t command_header;
|
||||
bool handled = false;
|
||||
|
||||
// Convert void to nullptr_t so that this compiles
|
||||
using Result2 = std::conditional_t<std::is_void_v<Result>, decltype(nullptr), Result>;
|
||||
std::optional<Result2> result = { };
|
||||
|
||||
template<typename Handler>
|
||||
void OnUnknown(Handler&& handler) {
|
||||
if (!handled) {
|
||||
if constexpr (std::is_void_v<Result>) {
|
||||
std::forward<Handler>(handler)();
|
||||
} else {
|
||||
result = std::forward<Handler>(handler)();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
template<bool IsResponse, typename Command, typename Handler, typename... ExtraParameters>
|
||||
void DecodeMessage(Handler&& handler, ExtraParameters&&... extras) {
|
||||
using CommandTags = std::conditional_t<IsResponse, typename Command::response_list, typename Command::request_list>;
|
||||
|
||||
IPC::TLSReader reader = { thread };
|
||||
if constexpr (std::is_void_v<Result>) {
|
||||
InvokeWithIPCArguments(std::forward<Handler>(handler), std::move(reader), CommandTags { }, std::forward<ExtraParameters>(extras)...);
|
||||
} else {
|
||||
result = InvokeWithIPCArguments(std::forward<Handler>(handler), std::move(reader), CommandTags { }, std::forward<ExtraParameters>(extras)...);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template<typename Command, typename Result = void>
|
||||
struct ResponseDispatcher : DispatcherBase<Result> {
|
||||
Session& session;
|
||||
|
||||
ResponseDispatcher(Thread& thread, Session& session)
|
||||
: DispatcherBase<Result> { thread, 0x0 /* TODO */ },
|
||||
session(session) {
|
||||
}
|
||||
|
||||
ResponseDispatcher(const ResponseDispatcher&) = delete;
|
||||
|
||||
template<typename Handler>
|
||||
void OnResponse(Handler&& handler) const {
|
||||
session.hpv_ipc_callback = [handler=std::forward<Handler>(handler)](Hypervisor& hypervisor, Thread& thread) mutable {
|
||||
// TODO: Verify thread.ReadTLS(0x80) == Command::response_header
|
||||
|
||||
IPC::TLSReader reader = { thread };
|
||||
using CommandTags = typename Command::response_list;
|
||||
return InvokeWithIPCArguments(std::forward<Handler>(handler), std::move(reader), CommandTags { }, hypervisor, thread);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
template<typename Result = void>
|
||||
struct RequestDispatcher : DispatcherBase<Result> {
|
||||
Session& session;
|
||||
|
||||
RequestDispatcher(Thread& thread, Session& session, uint32_t command_header)
|
||||
: DispatcherBase<Result> { thread, command_header },
|
||||
session(session) {
|
||||
}
|
||||
|
||||
template<typename Command, typename Handler>
|
||||
void DecodeRequest(Handler&& handler) {
|
||||
if (!this->handled && this->command_header == Command::request_header) {
|
||||
auto response_disp = ResponseDispatcher<Command>(this->thread, session);
|
||||
this->template DecodeMessage<false, Command>(std::forward<Handler>(handler), response_disp);
|
||||
this->handled = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace HPV
|
||||
|
||||
} // namespace OS
|
||||
|
||||
} // namespace HLE
|
10
source/os_serialization.cpp
Normal file
10
source/os_serialization.cpp
Normal file
|
@ -0,0 +1,10 @@
|
|||
#define FORMATS_IMPL_EXPLICIT_FORMAT_INSTANTIATIONS_INTENDED
|
||||
#include <framework/formats_impl.hpp>
|
||||
|
||||
#include "os.hpp"
|
||||
|
||||
namespace FileFormat {
|
||||
|
||||
template struct SerializationInterface<HLE::OS::CodeSetInfo>;
|
||||
|
||||
} // namespace FileFormat
|
35
source/os_serialization.hpp
Normal file
35
source/os_serialization.hpp
Normal file
|
@ -0,0 +1,35 @@
|
|||
#pragma once
|
||||
|
||||
#include "os.hpp"
|
||||
#include "framework/formats.hpp" // TODO: Rename contents to generic Serialization namespace
|
||||
|
||||
namespace Serialization {
|
||||
|
||||
inline auto Loader(HLE::OS::Thread& thread, /*VAddr*/ uint32_t vaddr) {
|
||||
return [&thread, vaddr](char* dest, size_t size) mutable {
|
||||
for (auto data_ptr = dest; data_ptr - dest < size; ++data_ptr) {
|
||||
*data_ptr = thread.ReadMemory(vaddr++);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// TODO: Move to common framework
|
||||
template<typename Data, typename... T>
|
||||
inline auto LoadVia(T&&... ts) {
|
||||
return FileFormat::SerializationInterface<Data>::Load(Loader(std::forward<T>(ts)...));
|
||||
}
|
||||
|
||||
// TODO: We can't implement this currently: Load() doesn't take a reference, but copy, hence the state of loader isn't carried on!
|
||||
//template<typename... Datas, typename... T>
|
||||
//inline auto LoadVia(T&&... ts) {
|
||||
// auto loader = Loader(std::forward<T>(ts)...);
|
||||
// if constexpr (sizeof... (Datas) == 1) {
|
||||
// // Return the element directly
|
||||
// return (FileFormat::SerializationInterface<Datas>::Load(loader), ...);
|
||||
// } else {
|
||||
// // Return a tuple of loaded elements
|
||||
// return std::tuple<Datas...> { FileFormat::SerializationInterface<Datas>::Load(loader)... };
|
||||
// }
|
||||
//}
|
||||
|
||||
} // namespace Serialization
|
109
source/os_types.hpp
Normal file
109
source/os_types.hpp
Normal file
|
@ -0,0 +1,109 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
namespace HLE {
|
||||
|
||||
namespace OS {
|
||||
|
||||
// Type of a virtual address
|
||||
// TODO: Make this a strong typedef
|
||||
using VAddr = uint32_t;
|
||||
|
||||
// Type of a physical address
|
||||
// TODO: Make this a strong typedef
|
||||
using PAddr = uint32_t;
|
||||
|
||||
// TODO: Make this a strong typedef
|
||||
using ProcessId = uint32_t;
|
||||
|
||||
// TODO: Make this a strong typedef
|
||||
using ThreadId = uint32_t;
|
||||
|
||||
/**
|
||||
* Type used to identify objects by a number internally
|
||||
* This is needed so that the emulated CPU core can pass around object handles
|
||||
* without actually storing objects in emulated memory.
|
||||
*/
|
||||
struct Handle {
|
||||
uint32_t value;
|
||||
|
||||
bool operator < (const Handle& other) const {
|
||||
return value < other.value;
|
||||
}
|
||||
|
||||
bool operator == (const Handle& other) const {
|
||||
return value == other.value;
|
||||
}
|
||||
|
||||
bool operator != (const Handle& other) const {
|
||||
return value != other.value;
|
||||
}
|
||||
};
|
||||
|
||||
const Handle HANDLE_INVALID = { 0 };
|
||||
|
||||
/**
|
||||
* Extension to the Handle type that stores additional debug information about
|
||||
* the handle. DebugHandle compares and hashes equal to the Handle it decays
|
||||
* to, so when it's used as a key into associative containers, regular Handles
|
||||
* are sufficient for lookup.
|
||||
*/
|
||||
struct DebugHandle : Handle {
|
||||
enum Source {
|
||||
Original, // The owning process created the underlying object
|
||||
Duplicated, // The owning process duplicated an existing handle
|
||||
FromIPC, // The owning process received the handle from another process via IPC
|
||||
Raw, // Obtained from emulated application, hence no valid debug information is attached
|
||||
};
|
||||
|
||||
struct DebugInfo {
|
||||
uint64_t created_time_stamp = 0;
|
||||
|
||||
Source source = Raw;
|
||||
|
||||
/// Header of the IPC request due to which the handle was transmitted (only valid if source is FromIPC)
|
||||
uint32_t ipc_command_header = 0;
|
||||
|
||||
// TODO: For Duplicated, store the original Handle value
|
||||
} debug_info;
|
||||
|
||||
DebugHandle() = default;
|
||||
|
||||
DebugHandle(uint32_t value, DebugInfo debug_info) : Handle{value}, debug_info(debug_info) {}
|
||||
|
||||
/// Create a DebugHandle from a plain handle, omitting the initialization of any debug information
|
||||
DebugHandle(const Handle& handle) : Handle(handle) {}
|
||||
|
||||
// bool operator == (const DebugHandle& other) const {
|
||||
// return value == other.value;
|
||||
// }
|
||||
|
||||
// bool operator == (const Handle& other) const {
|
||||
// return value == other.value;
|
||||
// }
|
||||
|
||||
// bool operator != (const Handle& other) const {
|
||||
// return value != other.value;
|
||||
// }
|
||||
};
|
||||
|
||||
} // namespace OS
|
||||
|
||||
} // namespace HLE
|
||||
|
||||
namespace std {
|
||||
template<>
|
||||
struct hash<HLE::OS::Handle> {
|
||||
auto operator()(const HLE::OS::Handle& handle) const {
|
||||
return std::hash<decltype(handle.value)>{}(handle.value);
|
||||
}
|
||||
};
|
||||
template<>
|
||||
struct hash<HLE::OS::DebugHandle> {
|
||||
auto operator()(const HLE::OS::DebugHandle& handle) const {
|
||||
return std::hash<HLE::OS::Handle>{}(handle);
|
||||
}
|
||||
};
|
||||
} // namespace std
|
37
source/pica.cpp
Normal file
37
source/pica.cpp
Normal file
|
@ -0,0 +1,37 @@
|
|||
// video_core has its own version of BitField that collides with our BitField. Hence, we initialize the video_core context in this separate translation unit, where we don't include BitField.
|
||||
|
||||
#include "pica.hpp"
|
||||
#include "video_core/src/video_core/vulkan/renderer.hpp"
|
||||
|
||||
#include "video_core/src/video_core/context.h"
|
||||
|
||||
#include <framework/profiler.hpp>
|
||||
|
||||
#include <spdlog/logger.h>
|
||||
|
||||
PicaContext::PicaContext( std::shared_ptr<spdlog::logger> logger, Debugger::DebugServer& debug_server,
|
||||
Settings::Settings& settings, Memory::PhysicalMemory& mem,
|
||||
Profiler::Profiler& profiler,vk::PhysicalDevice physical_device, vk::Device device,
|
||||
uint32_t graphics_queue_index, vk::Queue render_graphics_queue)
|
||||
: context(std::make_unique<Pica::Context>()) {
|
||||
renderer = std::make_unique<Pica::Vulkan::Renderer>(mem, logger, profiler, physical_device, device, graphics_queue_index, render_graphics_queue);
|
||||
context->debug_server = &debug_server;
|
||||
context->settings = &settings;
|
||||
context->renderer = renderer.get();
|
||||
context->activity = &profiler.GetActivity("GPU");
|
||||
context->logger = logger;
|
||||
}
|
||||
|
||||
PicaContext::~PicaContext() = default;
|
||||
|
||||
void PicaContext::InjectDependency(InterruptListener& os) {
|
||||
context->os = &os;
|
||||
}
|
||||
|
||||
void PicaContext::InjectDependency(Memory::PhysicalMemory& memory) {
|
||||
context->mem = &memory;
|
||||
}
|
||||
|
||||
Pica::Renderer* GetRenderer(Pica::Context& context) {
|
||||
return context.renderer;
|
||||
}
|
54
source/pica.hpp
Normal file
54
source/pica.hpp
Normal file
|
@ -0,0 +1,54 @@
|
|||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace spdlog {
|
||||
class logger;
|
||||
}
|
||||
|
||||
namespace vk {
|
||||
class PhysicalDevice;
|
||||
class Device;
|
||||
class Queue;
|
||||
}
|
||||
|
||||
namespace Pica {
|
||||
struct Context;
|
||||
class Renderer;
|
||||
}
|
||||
|
||||
namespace Debugger {
|
||||
class DebugServer;
|
||||
}
|
||||
|
||||
namespace Settings {
|
||||
struct Settings;
|
||||
}
|
||||
|
||||
namespace Profiler {
|
||||
class Profiler;
|
||||
}
|
||||
|
||||
class InterruptListener;
|
||||
|
||||
namespace Memory {
|
||||
struct PhysicalMemory;
|
||||
}
|
||||
|
||||
class PicaContext {
|
||||
public:
|
||||
PicaContext(std::shared_ptr<spdlog::logger>, Debugger::DebugServer&,
|
||||
Settings::Settings&, Memory::PhysicalMemory&,
|
||||
Profiler::Profiler&, vk::PhysicalDevice, vk::Device,
|
||||
uint32_t graphics_queue_index, vk::Queue render_graphics_queue);
|
||||
~PicaContext();
|
||||
|
||||
void InjectDependency(InterruptListener& listener);
|
||||
void InjectDependency(Memory::PhysicalMemory& memory);
|
||||
|
||||
std::unique_ptr<Pica::Context> context;
|
||||
std::unique_ptr<Pica::Renderer> renderer;
|
||||
};
|
||||
|
||||
// Interface functions so that we don't need to define Pica::Context in here...
|
||||
Pica::Renderer* GetRenderer(Pica::Context&);
|
5
source/platform/CMakeLists.txt
Normal file
5
source/platform/CMakeLists.txt
Normal file
|
@ -0,0 +1,5 @@
|
|||
add_library(platform STATIC file_formats/formats.cpp)
|
||||
#target_include_directories(platform PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/..>)
|
||||
target_include_directories(platform PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/..)
|
||||
target_link_libraries(platform framework)
|
||||
target_link_libraries(platform range-v3::range-v3)
|
102
source/platform/am.hpp
Normal file
102
source/platform/am.hpp
Normal file
|
@ -0,0 +1,102 @@
|
|||
#pragma once
|
||||
|
||||
#include "ipc.hpp"
|
||||
|
||||
namespace Platform {
|
||||
|
||||
/**
|
||||
* Application Manager: Interface used most prominently by the Home Menu to
|
||||
* get information about installed titles
|
||||
*/
|
||||
namespace AM {
|
||||
|
||||
// All IPC commands in this module have unique command IDs and hence
|
||||
// are not put in per-service namespaces
|
||||
|
||||
namespace IPC = Platform::IPC;
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Media Type
|
||||
*
|
||||
* Outputs:
|
||||
* - Number of title IDs for this media type
|
||||
*/
|
||||
struct GetNumPrograms : IPC::IPCCommand<0x1>::add_uint32
|
||||
::response::add_uint32 {};
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Maximum title count
|
||||
* - Media Type
|
||||
*
|
||||
* Outputs:
|
||||
* - Number of title IDs actually returned
|
||||
* - Title IDs
|
||||
*/
|
||||
struct GetProgramList : IPC::IPCCommand<0x2>::add_uint32::add_uint32::add_buffer_mapping_write
|
||||
::response::add_uint32::add_buffer_mapping_write {};
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Media Type
|
||||
* - Number of titles to query ProgramInfos for
|
||||
* - List of title IDs to query ProgramInfos for
|
||||
*
|
||||
* Outputs:
|
||||
* - ProgramInfos for each input title id
|
||||
*/
|
||||
struct GetProgramInfos : IPC::IPCCommand<0x3>::add_uint32::add_uint32::add_buffer_mapping_read::add_buffer_mapping_write
|
||||
::response::add_uint32::add_buffer_mapping_read::add_buffer_mapping_write {};
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Media Type
|
||||
* - Title ID
|
||||
*
|
||||
* Outputs:
|
||||
* - Number of content infos for this title
|
||||
*/
|
||||
struct GetDLCContentInfoCount : IPC::IPCCommand<0x1001>::add_uint32::add_uint64
|
||||
::response::add_uint32 {};
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Number of content infos to read
|
||||
* - Media Type
|
||||
* - Title ID
|
||||
* - Offset (index of first content info to read?)
|
||||
* - Output buffer
|
||||
*
|
||||
* Outputs:
|
||||
* - Number of content infos read
|
||||
*/
|
||||
struct ListDLCContentInfos : IPC::IPCCommand<0x1003>::add_uint32::add_uint32::add_uint64::add_uint32::add_buffer_mapping_write
|
||||
::response::add_uint32::add_buffer_mapping_write {};
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Media Type
|
||||
* - Number of titles in the input list
|
||||
* - Title IDs to get title infos for
|
||||
* - Output buffer for title infos
|
||||
*/
|
||||
struct GetDLCTitleInfos : IPC::IPCCommand<0x1005>::add_uint32::add_uint32::add_buffer_mapping_read::add_buffer_mapping_write
|
||||
::response::add_buffer_mapping_read::add_buffer_mapping_write {};
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Number of ticket infos to get
|
||||
* - Title ID
|
||||
* - Offset (index of first ticket to get?)
|
||||
* - Output buffer for ticket infos
|
||||
*
|
||||
* Outputs:
|
||||
* - Number of ticket infos read for this title
|
||||
*/
|
||||
struct ListDataTitleTicketInfos : IPC::IPCCommand<0x1007>::add_uint32::add_uint64::add_uint32::add_buffer_mapping_write
|
||||
::response::add_uint32::add_buffer_mapping_write {};
|
||||
|
||||
} // namespace AM
|
||||
|
||||
} // namespace Platform
|
24
source/platform/cdc.hpp
Normal file
24
source/platform/cdc.hpp
Normal file
|
@ -0,0 +1,24 @@
|
|||
#pragma once
|
||||
|
||||
#include "ipc.hpp"
|
||||
|
||||
namespace Platform {
|
||||
|
||||
/**
|
||||
* CDC = Communication Device Class.
|
||||
*/
|
||||
namespace CDC {
|
||||
|
||||
namespace HID {
|
||||
|
||||
// NOTE: Response headers for these aren't verified!
|
||||
using GetTouchData = IPC::IPCCommand<0x1>
|
||||
::response::add_uint32::add_uint32;
|
||||
using Initialize = IPC::IPCCommand<0x2>
|
||||
::response;
|
||||
|
||||
}
|
||||
|
||||
} // namespace CDC
|
||||
|
||||
} // namespace Platform
|
51
source/platform/config.hpp
Normal file
51
source/platform/config.hpp
Normal file
|
@ -0,0 +1,51 @@
|
|||
#pragma once
|
||||
|
||||
#include "ipc.hpp"
|
||||
|
||||
namespace Platform {
|
||||
|
||||
/**
|
||||
* Config: Exposes access to various global configuration parameters, most
|
||||
* of which are stored in this title's NAND savegame.
|
||||
*/
|
||||
namespace Config {
|
||||
|
||||
// Lots of these commands are shared between cfg:u, cfg:i, and cfg:s, hence we
|
||||
// make no effort to separating them into different namespaces
|
||||
|
||||
using GetConfigInfoBlk2 = Platform::IPC::IPCCommand<0x1>::add_uint32::add_uint32::add_buffer_mapping_write
|
||||
::response;
|
||||
|
||||
using SecureInfoGetRegion = Platform::IPC::IPCCommand<0x2>
|
||||
::response::add_uint32;
|
||||
|
||||
/**
|
||||
* Checks whether the system is running on a Canada or USA region.
|
||||
*
|
||||
* This information is queried from nand:/rw/sys/SecureInfo_A and from the
|
||||
* CountryInfo savedata block. 1 is returned if both indicate USA/Canada.
|
||||
*/
|
||||
using RegionIsCanadaOrUSA = Platform::IPC::IPCCommand<0x4>
|
||||
::response::add_uint32;
|
||||
|
||||
using SecureInfoGetRegion2 = Platform::IPC::IPCCommand<0x406>
|
||||
::response::add_uint32;
|
||||
using SecureInfoGetRegion3 = Platform::IPC::IPCCommand<0x816>
|
||||
::response::add_uint32;
|
||||
|
||||
using GetSystemModel = Platform::IPC::IPCCommand<0x5>
|
||||
::response::add_uint32;
|
||||
using GetConfigInfoBlk8 = Platform::IPC::IPCCommand<0x401>::add_uint32::add_uint32::add_buffer_mapping_write
|
||||
::response::add_buffer_mapping_write;
|
||||
/**
|
||||
* Inputs:
|
||||
* - Block id
|
||||
* - Size of data to be written
|
||||
* - Input data
|
||||
*/
|
||||
using SetConfigInfoBlk4 = Platform::IPC::IPCCommand<0x402>::add_uint32::add_uint32::add_buffer_mapping_read
|
||||
::response::add_buffer_mapping_read;
|
||||
|
||||
} // namespace Config
|
||||
|
||||
} // namespace Platform
|
21
source/platform/crypto.hpp
Normal file
21
source/platform/crypto.hpp
Normal file
|
@ -0,0 +1,21 @@
|
|||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
struct KeyDatabase {
|
||||
// Each AES slot has three entries called KeyX, KeyY, and KeyN ("normal key")
|
||||
static constexpr int num_aes_slots = 0x40;
|
||||
|
||||
using KeyType = std::array<uint8_t, 16>;
|
||||
|
||||
struct {
|
||||
std::optional<KeyType> x;
|
||||
std::optional<KeyType> y;
|
||||
std::optional<KeyType> n;
|
||||
} aes_slots[num_aes_slots];
|
||||
|
||||
// KeyYs used to decrypt title keys (required to decrypt CDN contents and update CIAs)
|
||||
std::array<std::optional<KeyType>, 6> common_y;
|
||||
};
|
13
source/platform/csnd.hpp
Normal file
13
source/platform/csnd.hpp
Normal file
|
@ -0,0 +1,13 @@
|
|||
#pragma once
|
||||
|
||||
#include "ipc.hpp"
|
||||
|
||||
namespace Platform::CSND {
|
||||
|
||||
using Initialize = IPC::IPCCommand<0x1>::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32
|
||||
::response::add_handles<IPC::HandleType::Object, 2>;
|
||||
|
||||
using FlushDataCache = IPC::IPCCommand<0x9>::add_uint32::add_uint32::add_handle<IPC::HandleType::Process>
|
||||
::response;
|
||||
|
||||
} // namespace Platform::CSND
|
62
source/platform/dsp.hpp
Normal file
62
source/platform/dsp.hpp
Normal file
|
@ -0,0 +1,62 @@
|
|||
#pragma once
|
||||
|
||||
#include "../ipc.hpp"
|
||||
|
||||
namespace Platform {
|
||||
|
||||
namespace CTR {
|
||||
|
||||
namespace DSP {
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Semaphore value (TODOTEST: upper 16 bit ignored?)
|
||||
*/
|
||||
using SetSemaphore = HLE::IPC::IPCCommand<0x7>::add_uint32
|
||||
::response;
|
||||
|
||||
using WriteProcessPipe = HLE::IPC::IPCCommand<0xd>::add_uint32::add_uint32::add_static_buffer
|
||||
::response;
|
||||
|
||||
using ReadPipeIfPossible = HLE::IPC::IPCCommand<0x10>::add_uint32::add_uint32::add_uint32
|
||||
::response::add_uint32::add_static_buffer;
|
||||
|
||||
using LoadComponent = HLE::IPC::IPCCommand<0x11>::add_uint32::add_uint32::add_uint32::add_buffer_mapping_read
|
||||
::response::add_uint32::add_buffer_mapping_read;
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Virtual base address
|
||||
* - Number of bytes
|
||||
* - Process owning the flushed memory
|
||||
*/
|
||||
using FlushDataCache = HLE::IPC::IPCCommand<0x13>::add_uint32::add_uint32::add_handle<IPC::HandleType::Process>
|
||||
::response;
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Virtual base address
|
||||
* - Number of bytes
|
||||
* - Process owning the invalidated memory
|
||||
*/
|
||||
using InvalidateDataCache = HLE::IPC::IPCCommand<0x14>::add_uint32::add_uint32::add_handle<IPC::HandleType::Process>
|
||||
::response;
|
||||
|
||||
using RegisterInterruptEvents = HLE::IPC::IPCCommand<0x15>::add_uint32::add_uint32::add_handle<HLE::IPC::HandleType::Event>
|
||||
::response;
|
||||
|
||||
/**
|
||||
* Outputs:
|
||||
* - Event for the application to signal whenever it has written data for an audio frame
|
||||
*/
|
||||
using GetSemaphoreEventHandle = HLE::IPC::IPCCommand<0x16>
|
||||
::response::add_handle<HLE::IPC::HandleType::Event>;
|
||||
|
||||
using SetSemaphoreMask = HLE::IPC::IPCCommand<0x17>::add_uint32
|
||||
::response;
|
||||
|
||||
} // namespace DSP
|
||||
|
||||
} // namespace CTR
|
||||
|
||||
} // namespace Platform
|
102
source/platform/file_formats/3dsx.hpp
Normal file
102
source/platform/file_formats/3dsx.hpp
Normal file
|
@ -0,0 +1,102 @@
|
|||
#pragma once
|
||||
|
||||
#include <framework/formats.hpp>
|
||||
|
||||
#include <boost/hana/define_struct.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
namespace FileFormat {
|
||||
|
||||
/**
|
||||
* 3DSX file format specification: 3DSX is a community-invented file format
|
||||
* used for running userland homebrew applications on the 3DS.
|
||||
*/
|
||||
namespace Dot3DSX {
|
||||
|
||||
/**
|
||||
* Main 3DSX header. This header is followed by:
|
||||
* - an optional secondary header (referred to as "3DSX extended header", not to be confused with NCCH extended headers)
|
||||
* - three RelocationHeaders (one each for the code, rodata, and data segments)
|
||||
* - the code and rodata segments
|
||||
* - the data segment (without bss TODO: Double check!)
|
||||
* - a sequence of RelocationDescriptors for each program segment
|
||||
*/
|
||||
struct Header {
|
||||
BOOST_HANA_DEFINE_STRUCT(Header,
|
||||
(std::array<uint8_t, 4>, magic), // "3DSX"
|
||||
(uint16_t, header_size), // Size of this header and (if enabled) the extended header
|
||||
(uint16_t, reloc_header_size), // Size of the RelocationHeader
|
||||
(uint32_t, version), // Version (always 0 as of the time of writing)
|
||||
(uint32_t, flags), // Not used for anything
|
||||
|
||||
// Segment sizes (in bytes)
|
||||
(uint32_t, text_size),
|
||||
(uint32_t, ro_size),
|
||||
(uint32_t, data_bss_size), // Data and bss section
|
||||
(uint32_t, bss_size) // Bss section, only
|
||||
);
|
||||
|
||||
uint32_t TextOffset() const {
|
||||
return header_size + 3 * reloc_header_size;
|
||||
}
|
||||
|
||||
uint32_t RoOffset() const {
|
||||
return TextOffset() + text_size;
|
||||
}
|
||||
|
||||
uint32_t DataOffset() const {
|
||||
return RoOffset() + ro_size;
|
||||
}
|
||||
|
||||
uint32_t RelocationHeaderOffset(unsigned int index) const {
|
||||
return header_size + reloc_header_size * index;
|
||||
}
|
||||
|
||||
uint32_t CodeRelocationInfoOffset() const {
|
||||
return DataOffset() + (data_bss_size - bss_size);
|
||||
}
|
||||
|
||||
struct Tags : little_endian_tag, expected_size_tag<0x20> {};
|
||||
|
||||
// Supported in GCC 7.1, but not in older ones. Not exactly sure at which point specifically they added support
|
||||
#if !defined(__GNUC__) || __GNUC__ >= 7
|
||||
// Identifying word at the file beginning
|
||||
static inline constexpr std::array<char, 4> expected_magic = {{ '3', 'D', 'S', 'X' }};
|
||||
#endif
|
||||
};
|
||||
|
||||
/**
|
||||
* 3DSX secondary header (optional)
|
||||
*/
|
||||
struct SecondaryHeader {
|
||||
BOOST_HANA_DEFINE_STRUCT(SecondaryHeader,
|
||||
(uint32_t, smdh_offset), // In bytes
|
||||
(uint32_t, smdh_size), // In bytes
|
||||
(uint32_t, romfs_offset) // In bytes (All remaining data in the file is considered part of RomFS, hence implicitly defining its size)
|
||||
);
|
||||
};
|
||||
|
||||
struct RelocationHeader {
|
||||
BOOST_HANA_DEFINE_STRUCT(RelocationHeader,
|
||||
(uint32_t, num_abs_relocs),
|
||||
(uint32_t, num_rel_relocs)
|
||||
);
|
||||
|
||||
struct Tags : little_endian_tag, expected_size_tag<0x8> {};
|
||||
};
|
||||
static_assert(sizeof(RelocationHeader) == 8, "Structure has incorrect size");
|
||||
|
||||
struct RelocationInfo {
|
||||
BOOST_HANA_DEFINE_STRUCT(RelocationInfo,
|
||||
(uint16_t, words_to_skip),
|
||||
(uint16_t, words_to_patch)
|
||||
);
|
||||
|
||||
struct Tags : little_endian_tag, expected_size_tag<0x4> {};
|
||||
};
|
||||
|
||||
} // namespace Dot3DSX
|
||||
|
||||
} // namespace FileFormat
|
71
source/platform/file_formats/bcfnt.hpp
Normal file
71
source/platform/file_formats/bcfnt.hpp
Normal file
|
@ -0,0 +1,71 @@
|
|||
#pragma once
|
||||
|
||||
#include <framework/formats.hpp>
|
||||
|
||||
#include <boost/hana/define_struct.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
namespace FileFormat::BCFNT {
|
||||
|
||||
struct Header {
|
||||
BOOST_HANA_DEFINE_STRUCT(Header,
|
||||
(std::array<uint8_t, 4>, magic), // "CFNT" (on filesystem) or "CFNU" (decompressed in NS shared memory)
|
||||
(std::array<uint8_t, 2>, unknown),
|
||||
(uint16_t, header_size_bytes),
|
||||
(uint32_t, version),
|
||||
(uint32_t, file_size_bytes),
|
||||
(uint32_t, num_blocks)
|
||||
);
|
||||
};
|
||||
|
||||
// Common structure shared between block headers defined below
|
||||
struct BlockCommon {
|
||||
BOOST_HANA_DEFINE_STRUCT(BlockCommon,
|
||||
(std::array<uint8_t, 4>, magic),
|
||||
(uint32_t, num_bytes) // Number of bytes from block start to the next block; may include data following the headers below
|
||||
);
|
||||
};
|
||||
|
||||
struct FINF {
|
||||
static constexpr std::array<uint8_t, 4> magic = { 'F', 'I', 'N', 'F' };
|
||||
|
||||
BOOST_HANA_DEFINE_STRUCT(FINF,
|
||||
(std::array<uint8_t, 0x8>, unknown1),
|
||||
(uint32_t, tglp_offset),
|
||||
(uint32_t, cwdh_offset),
|
||||
(uint32_t, cmap_offset),
|
||||
(std::array<uint8_t, 0x4>, unknown2)
|
||||
);
|
||||
};
|
||||
static_assert(sizeof(FINF) == 24);
|
||||
|
||||
struct CMAP {
|
||||
static constexpr std::array<uint8_t, 4> magic = { 'C', 'M', 'A', 'P' };
|
||||
|
||||
BOOST_HANA_DEFINE_STRUCT(CMAP,
|
||||
(std::array<uint8_t, 0x8>, unknown),
|
||||
(uint32_t, next_block_offset)
|
||||
);
|
||||
};
|
||||
|
||||
struct CWDH {
|
||||
static constexpr std::array<uint8_t, 4> magic = { 'C', 'W', 'D', 'H' };
|
||||
|
||||
BOOST_HANA_DEFINE_STRUCT(CWDH,
|
||||
(std::array<uint8_t, 0x4>, unknown),
|
||||
(uint32_t, next_block_offset)
|
||||
);
|
||||
};
|
||||
|
||||
struct TGLP {
|
||||
static constexpr std::array<uint8_t, 4> magic = { 'T', 'G', 'L', 'P' };
|
||||
|
||||
BOOST_HANA_DEFINE_STRUCT(TGLP,
|
||||
(std::array<uint8_t, 0x14>, unknown),
|
||||
(uint32_t, data_offset)
|
||||
);
|
||||
};
|
||||
|
||||
} // namespace FileFormat::BCFNT
|
240
source/platform/file_formats/cia.hpp
Normal file
240
source/platform/file_formats/cia.hpp
Normal file
|
@ -0,0 +1,240 @@
|
|||
#pragma once
|
||||
|
||||
#include <framework/formats.hpp>
|
||||
|
||||
#include <boost/hana/define_struct.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace FileFormat {
|
||||
|
||||
struct CIAHeader {
|
||||
BOOST_HANA_DEFINE_STRUCT(CIAHeader,
|
||||
(uint32_t, header_size),
|
||||
(uint16_t, type), // TODO: Enumize
|
||||
(uint16_t, version),
|
||||
(uint32_t, certificate_chain_size), // in bytes
|
||||
(uint32_t, ticket_size),
|
||||
(uint32_t, tmd_size),
|
||||
(uint32_t, meta_size),
|
||||
(uint64_t, content_size)
|
||||
);
|
||||
|
||||
// Followed by:
|
||||
// * 0x2000 bytes of content mask data
|
||||
// * Certificate chain (first one verifying the other two certificates, second one verifying the ticket signature, third one verifying the tmd signature)
|
||||
// * Ticket (per-title "permission" to launch the title, including the encrypted titlekey)
|
||||
// * Title Meta Data
|
||||
// * content data (NCCHs or SRLs)
|
||||
// * these are encrypted using 128-bit AES-CBC with the titlekey iff the corresponding flag is set in the TMD (3dbrew erratum: This means they need not always be encrypted, although in practice they are for official content)
|
||||
// * meta file data
|
||||
|
||||
// NOTE: This is indeed little-endian according to 3dbrew...
|
||||
struct Tags : little_endian_tag, expected_size_tag<0x20> {};
|
||||
};
|
||||
|
||||
struct Signature {
|
||||
struct Tags : big_endian_tag {};
|
||||
|
||||
uint32_t type; // TODO: Enumize
|
||||
std::vector<uint8_t> data; // padded such that the CertificateSignature ends at multiples of 0x40 bytes
|
||||
|
||||
// TODO: Merge the signature issue into this struct!
|
||||
};
|
||||
|
||||
static size_t GetSignatureSize(uint32_t signature_type) {
|
||||
switch (signature_type) {
|
||||
case 0x10000:
|
||||
case 0x10003:
|
||||
return 0x200;
|
||||
|
||||
case 0x10001:
|
||||
case 0x10004:
|
||||
return 0x100;
|
||||
|
||||
case 0x10002:
|
||||
case 0x10005:
|
||||
return 0x3c;
|
||||
|
||||
default:
|
||||
throw std::runtime_error("Unknown signature type " + std::to_string(signature_type));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Consider BOOST_HANA_DEFINE_STRUCT-izing this union
|
||||
// NOTE: Possible 3dbrew erratum: Improper padding for these structs. they should be padded to 0x40 bytes
|
||||
union CertificatePublicKey {
|
||||
template<size_t bytes>
|
||||
struct RSAKey {
|
||||
BOOST_HANA_DEFINE_STRUCT(RSAKey<bytes>,
|
||||
(std::array<uint8_t, bytes>, modulus),
|
||||
(std::array<uint8_t, 4>, exponent), // TODO: Is this actually a uint32_t?
|
||||
(std::array<uint8_t, 0x34 + (bytes == 2057/8) * 0x100>, padding) // TODO: Figure out whether the 2048 key is padded like this...
|
||||
);
|
||||
};
|
||||
|
||||
RSAKey<2048/8> rsa2048;
|
||||
RSAKey<4096/8> rsa4096;
|
||||
|
||||
struct ECCKey {
|
||||
BOOST_HANA_DEFINE_STRUCT(ECCKey,
|
||||
(std::array<uint8_t, 0x3c>, key),
|
||||
(std::array<uint8_t, 0x3c>, padding)
|
||||
);
|
||||
} ecc;
|
||||
|
||||
// Must be padded like this to guarantee the overall certificate data adds up to multiples of 0x40 bytes
|
||||
static_assert((sizeof(rsa2048) % 0x40) == 0x38);
|
||||
static_assert((sizeof(rsa4096) % 0x40) == 0x38);
|
||||
static_assert((sizeof(ecc) % 0x40) == 0x38);
|
||||
};
|
||||
|
||||
static size_t GetCertificatePublicKeySize(uint32_t type) {
|
||||
switch (type) {
|
||||
case 0: return sizeof(std::declval<CertificatePublicKey>().rsa4096);
|
||||
case 1: return sizeof(std::declval<CertificatePublicKey>().rsa2048);
|
||||
case 2: return sizeof(std::declval<CertificatePublicKey>().ecc);
|
||||
default:
|
||||
throw std::runtime_error("Unknown certificate public key type " + std::to_string(type));
|
||||
}
|
||||
}
|
||||
|
||||
struct Certificate {
|
||||
Signature sig;
|
||||
|
||||
struct Data {
|
||||
BOOST_HANA_DEFINE_STRUCT(Data,
|
||||
(std::array<uint8_t, 0x40>, issuer),
|
||||
(uint32_t, key_type),
|
||||
(std::array<uint8_t, 0x40>, name),
|
||||
(uint32_t, unknown)
|
||||
);
|
||||
|
||||
struct Tags : big_endian_tag {};
|
||||
} cert;
|
||||
|
||||
CertificatePublicKey pubkey;
|
||||
};
|
||||
|
||||
struct TicketTimeLimit {
|
||||
BOOST_HANA_DEFINE_STRUCT(TicketTimeLimit,
|
||||
(uint32_t, enable),
|
||||
(uint32_t, limit_in_seconds)
|
||||
);
|
||||
};
|
||||
|
||||
struct Ticket {
|
||||
Signature sig;
|
||||
|
||||
struct Data {
|
||||
BOOST_HANA_DEFINE_STRUCT(Data,
|
||||
(std::array<unsigned char, 0x40>, issuer),
|
||||
(std::array<uint8_t, 0x3c>, signature_pubkey), // Only for ECDSA mode?
|
||||
(uint8_t, version), // 1 for 3DS
|
||||
(std::array<uint8_t, 2>, unknown),
|
||||
// The title key is encrypted using a keyscrambler pair of AES keyslot 0x3D keyX and a keyY selected by key_y_index
|
||||
(std::array<uint8_t, 0x10>, title_key_encrypted),
|
||||
(uint8_t, unknown2),
|
||||
(uint64_t, ticket_id),
|
||||
(uint32_t, console_id),
|
||||
(uint64_t, title_id),
|
||||
(uint16_t, sys_access), // ???
|
||||
(uint16_t, title_version),
|
||||
(uint32_t, time_mask), // ???
|
||||
(uint32_t, permit_mask), // ???
|
||||
(uint8_t, title_export), // called "license type" on 3dbrew???
|
||||
// index into a Process9-internal table of keyYs used to de/encrypt the title key
|
||||
(uint8_t, key_y_index),
|
||||
(std::array<uint8_t, 0x72>, unknown3),
|
||||
(std::array<TicketTimeLimit, 8>, time_limit),
|
||||
(std::array<uint8_t, 0xac>, unknown4) // "Content Index" on 3dbrew
|
||||
);
|
||||
|
||||
struct Tags : big_endian_tag, expected_size_tag<0x210> {};
|
||||
} data;
|
||||
};
|
||||
|
||||
/**
|
||||
* Title Meta Data
|
||||
*/
|
||||
struct TMD {
|
||||
Signature sig;
|
||||
|
||||
// Semantics need to be verified
|
||||
struct ContentInfoHash {
|
||||
BOOST_HANA_DEFINE_STRUCT(ContentInfoHash,
|
||||
(uint16_t, index_offset), // ???
|
||||
(uint16_t, chunk_count),
|
||||
(std::array<uint8_t, 0x20>, sha256) // hash over the next "chunk_count" content infos
|
||||
);
|
||||
|
||||
struct Tags : big_endian_tag, expected_size_tag<0x24> {};
|
||||
};
|
||||
|
||||
// Semantics need to be verified
|
||||
struct ContentInfo {
|
||||
BOOST_HANA_DEFINE_STRUCT(ContentInfo,
|
||||
// ID of the described content. Used to name .app files in the NAND filesystem
|
||||
(uint32_t, id),
|
||||
// 0=Main application, 1=Manual, 2=DLP child container ... not sure if this is by convention! 3dbrew suggests it might be used as an index into the CIA contents
|
||||
(uint16_t, index),
|
||||
(uint16_t, type), // Collection of flags: encrypted, disc, ...
|
||||
(uint64_t, size), // in bytes
|
||||
(std::array<uint8_t, 0x20>, sha256)
|
||||
);
|
||||
|
||||
struct Tags : big_endian_tag, expected_size_tag<0x30> {};
|
||||
};
|
||||
|
||||
struct Data {
|
||||
BOOST_HANA_DEFINE_STRUCT(Data,
|
||||
(std::array<unsigned char, 0x40>, issuer),
|
||||
(uint8_t, version), // Always 1?
|
||||
(uint8_t, ca_crl_version), // Version for certificate revocation list
|
||||
(uint8_t, signer_crl_version), // ???
|
||||
(uint8_t, unknown),
|
||||
(uint64_t, system_version),
|
||||
(uint64_t, title_id),
|
||||
(uint32_t, title_type), // ???
|
||||
(uint16_t, group_id), // ???
|
||||
(uint32_t, save_data_size), // in bytes; used for SRL Public Save Data Size for DS titles
|
||||
(uint32_t, srl_private_data_size),
|
||||
(uint32_t, unknown2),
|
||||
(uint8_t, srl_flag),
|
||||
(std::array<uint8_t, 0x31>, unknown3),
|
||||
(uint32_t, access_rights),
|
||||
(uint16_t, title_version),
|
||||
(uint16_t, content_count), // Number of encrypted NCCHs used by this title
|
||||
(uint16_t, main_content), // NCCH to boot upon launch
|
||||
(uint16_t, unknown4),
|
||||
(std::array<uint8_t, 0x20>, content_info_records_sha256) // Hash over content_info_hashes
|
||||
);
|
||||
|
||||
struct Tags : big_endian_tag, expected_size_tag<0xC4> {};
|
||||
} data;
|
||||
|
||||
// NOTE: Due to the convoluted hashing involved in generating a TMD,
|
||||
// it is more practical to move these out of the Data struct
|
||||
std::array<ContentInfoHash, 64> content_info_hashes;
|
||||
|
||||
// Followed by content_count of these:
|
||||
// TODO: Are these indeed dynamically sized?
|
||||
std::vector<ContentInfo> content_infos;
|
||||
};
|
||||
|
||||
struct CIAMeta {
|
||||
BOOST_HANA_DEFINE_STRUCT(CIAMeta,
|
||||
/// List of title IDs this application depends on
|
||||
(std::array<uint64_t, 0x30>, dependencies),
|
||||
(std::array<uint8_t, 0x180>, unknown),
|
||||
(uint32_t, core_version),
|
||||
(std::array<uint8_t, 0xfc>, unknown2),
|
||||
(std::array<uint8_t, 0x36c0>, icon_data)
|
||||
);
|
||||
};
|
||||
|
||||
} // namespace FileFormat
|
58
source/platform/file_formats/dsp1.hpp
Normal file
58
source/platform/file_formats/dsp1.hpp
Normal file
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* DSP firmware; uses file extension "cdc"
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <framework/bit_field_new.hpp>
|
||||
#include <framework/formats.hpp>
|
||||
|
||||
#include <boost/hana/define_struct.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace FileFormat {
|
||||
|
||||
struct DSPFirmwareHeader {
|
||||
struct InitFlags {
|
||||
BOOST_HANA_DEFINE_STRUCT(InitFlags,
|
||||
(uint8_t, storage)
|
||||
);
|
||||
|
||||
// If set, wait for DSP replies with data == 1 to be sent on each channel
|
||||
auto wait_for_dsp_reply() const { return BitField::v3::MakeFlagOn<0>(this); }
|
||||
|
||||
// If set, read 3d filter data from config module
|
||||
auto load_3d_filters() const { return BitField::v3::MakeFlagOn<1>(this); }
|
||||
};
|
||||
|
||||
struct SegmentInfo {
|
||||
BOOST_HANA_DEFINE_STRUCT(SegmentInfo,
|
||||
(uint32_t, data_offset), // offset in bytes of segment contents from the start of this header
|
||||
(uint32_t, target_offset), // target offset, 16-bit words
|
||||
(uint32_t, size_bytes),
|
||||
(std::array<uint8_t, 3>, unknown),
|
||||
(uint8_t, memory_type), // memory type; 0/1: program memory, 2: data memory
|
||||
(std::array<uint8_t, 32>, hash) // SHA256 hash over segment contents
|
||||
);
|
||||
};
|
||||
|
||||
BOOST_HANA_DEFINE_STRUCT(DSPFirmwareHeader,
|
||||
(std::array<uint8_t, 0x100>, signature), // RSA signature over the rest of this header
|
||||
(std::array<uint8_t, 4>, magic), // always "DSP1"
|
||||
(uint32_t, size_bytes), // size including header
|
||||
(uint16_t, memory_regions_mask), // mask enabling 32 kB regions in DSP memory
|
||||
(std::array<uint8_t, 3>, unknown),
|
||||
(uint8_t, memory_type_3d_filters), // memory type for 3D filter segment
|
||||
(uint8_t, num_segments), // number of entries to use from the segments array
|
||||
(InitFlags, init_flags), // flags to apply during loading
|
||||
(uint32_t, start_offset_3d_filters), // counted in 16-bit words. TODO: Is this from the start of DSP memory?
|
||||
(uint32_t, size_bytes_3d_filters), // size of 3d filters segment
|
||||
(uint64_t, unknown2),
|
||||
(std::array<SegmentInfo, 10>, segments)
|
||||
);
|
||||
|
||||
struct Tags : expected_size_tag<0x300> {};
|
||||
};
|
||||
|
||||
} // namespace FileFormat
|
74
source/platform/file_formats/formats.cpp
Normal file
74
source/platform/file_formats/formats.cpp
Normal file
|
@ -0,0 +1,74 @@
|
|||
#define FORMATS_IMPL_EXPLICIT_FORMAT_INSTANTIATIONS_INTENDED
|
||||
#include <framework/formats_impl.hpp>
|
||||
|
||||
#include "3dsx.hpp"
|
||||
#include "bcfnt.hpp"
|
||||
#include "cia.hpp"
|
||||
#include "dsp1.hpp"
|
||||
#include "ncch.hpp"
|
||||
|
||||
namespace FileFormat {
|
||||
|
||||
// 3DSX structures
|
||||
template struct SerializationInterface<Dot3DSX::Header>;
|
||||
template struct SerializationInterface<Dot3DSX::RelocationHeader>;
|
||||
template struct SerializationInterface<Dot3DSX::RelocationInfo>;
|
||||
|
||||
// BCFNT structures
|
||||
template struct SerializationInterface<BCFNT::Header>;
|
||||
template struct SerializationInterface<BCFNT::BlockCommon>;
|
||||
template struct SerializationInterface<BCFNT::FINF>;
|
||||
template struct SerializationInterface<BCFNT::CMAP>;
|
||||
template struct SerializationInterface<BCFNT::CWDH>;
|
||||
template struct SerializationInterface<BCFNT::TGLP>;
|
||||
|
||||
// CIA structures
|
||||
template struct SerializationInterface<Certificate::Data>;
|
||||
template struct SerializationInterface<CertificatePublicKey::RSAKey<2048/8>>;
|
||||
template struct SerializationInterface<CertificatePublicKey::RSAKey<4096/8>>;
|
||||
template struct SerializationInterface<CertificatePublicKey::ECCKey>;
|
||||
template struct SerializationInterface<CIAHeader>;
|
||||
template struct SerializationInterface<CIAMeta>;
|
||||
template struct SerializationInterface<Ticket::Data>;
|
||||
template struct SerializationInterface<TicketTimeLimit>;
|
||||
template struct SerializationInterface<TMD::ContentInfo>;
|
||||
template struct SerializationInterface<TMD::ContentInfoHash>;
|
||||
template struct SerializationInterface<TMD::Data>;
|
||||
|
||||
// DSP structures
|
||||
template struct SerializationInterface<DSPFirmwareHeader>;
|
||||
|
||||
// NCCH structures
|
||||
template struct SerializationInterface<ExeFSHeader>;
|
||||
template struct SerializationInterface<ExHeader>;
|
||||
template struct SerializationInterface<NCCHHeader>;
|
||||
|
||||
template<>
|
||||
uint32_t SerializationInterface<uint32_t>::Load(std::function<void(char*, size_t)> reader) {
|
||||
return ConstructFromReadCallback<uint32_t, detail::DefaultTags, std::tuple<uint32_t>>{}(reader);
|
||||
}
|
||||
|
||||
template<>
|
||||
uint64_t SerializationInterface<uint64_t>::Load(std::function<void(char*, size_t)> reader) {
|
||||
return ConstructFromReadCallback<uint64_t, detail::DefaultTags, std::tuple<uint64_t>>{}(reader);
|
||||
}
|
||||
|
||||
template<>
|
||||
boost::endian::little_uint32_t SerializationInterface<boost::endian::little_uint32_t>::Load(std::function<void(char*, size_t)> reader) {
|
||||
struct Tags : little_endian_tag {};
|
||||
return ConstructFromReadCallback<uint32_t, Tags, std::tuple<uint32_t>>{}(reader);
|
||||
}
|
||||
|
||||
template<>
|
||||
boost::endian::big_uint32_t SerializationInterface<boost::endian::big_uint32_t>::Load(std::function<void(char*, size_t)> reader) {
|
||||
struct Tags : big_endian_tag {};
|
||||
return ConstructFromReadCallback<uint32_t, Tags, std::tuple<uint32_t>>{}(reader);
|
||||
}
|
||||
|
||||
template<>
|
||||
void SerializationInterface<boost::endian::big_uint32_t>::Save(const boost::endian::big_uint32_t& data, std::function<void(char*, size_t)> writer) {
|
||||
struct Tags : big_endian_tag {};
|
||||
SaveWithWriteCallback<uint32_t, Tags, std::tuple<uint32_t>>{}(data, writer);
|
||||
}
|
||||
|
||||
} // namespace FileFormat
|
482
source/platform/file_formats/ncch.hpp
Normal file
482
source/platform/file_formats/ncch.hpp
Normal file
|
@ -0,0 +1,482 @@
|
|||
#pragma once
|
||||
|
||||
#include <framework/bit_field_new.hpp>
|
||||
#include <framework/formats.hpp>
|
||||
|
||||
#include <range/v3/algorithm/max.hpp>
|
||||
#include <range/v3/view/transform.hpp>
|
||||
|
||||
#include <boost/endian/arithmetic.hpp>
|
||||
#include <boost/hana/define_struct.hpp>
|
||||
|
||||
#include <boost/mp11.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
namespace FileFormat {
|
||||
|
||||
using namespace boost::endian;
|
||||
|
||||
// Strong typedef of the given argument type to represent a media unit (0x200 bytes)
|
||||
template<typename T>
|
||||
struct MediaUnit {
|
||||
BOOST_HANA_DEFINE_STRUCT(MediaUnit<T>,
|
||||
(T, raw)
|
||||
);
|
||||
|
||||
// TODO: Static assert this is representable
|
||||
uint64_t ToBytes() const {
|
||||
return uint64_t{raw} * 0x200;
|
||||
}
|
||||
|
||||
static MediaUnit FromBytes(uint64_t bytes) {
|
||||
return { static_cast<T>(bytes / 0x200) };
|
||||
}
|
||||
};
|
||||
using MediaUnit32 = MediaUnit<uint32_t>;
|
||||
|
||||
using Bytes32 = uint32_t;
|
||||
|
||||
struct NCCHHeader {
|
||||
BOOST_HANA_DEFINE_STRUCT(NCCHHeader,
|
||||
(std::array<uint8_t, 0x100>, signature), // RSA-2048 signature
|
||||
(std::array<uint8_t, 4>, magic), // always "NCCH"
|
||||
|
||||
(MediaUnit32, content_size), // total size of this file
|
||||
|
||||
(uint64_t, partition_id), // Always the same as the title id?
|
||||
|
||||
(std::array<uint8_t, 0x8>, unknown),
|
||||
(uint64_t, program_id), // Always the same as the title id?
|
||||
|
||||
(std::array<uint8_t, 0x10>, unknown2a),
|
||||
|
||||
// Only since system version 5.0 (TODO: Is there some indicator for the presence of this field?)
|
||||
(std::array<uint8_t, 0x20>, logo_sha256),
|
||||
|
||||
(std::array<uint8_t, 0x10>, unknown2b),
|
||||
|
||||
(std::array<uint8_t, 0x20>, exheader_sha256),
|
||||
|
||||
// Reduced size of the extended header: Only refers to System Control Info and Access Control Info
|
||||
(Bytes32, exheader_size),
|
||||
|
||||
(std::array<uint8_t, 0x7>, unknown3),
|
||||
|
||||
// If non-zero, an additional layer of encryption is used for RomFS and parts of ExeFS
|
||||
(uint8_t, crypto_method),
|
||||
|
||||
// 1 = Old3DS, 2 = New3DS
|
||||
(uint8_t, platform),
|
||||
|
||||
// 1 = data, 2 = executable, 4 = system update, 8 = manual, 0x10 = trial
|
||||
(uint8_t, type_mask),
|
||||
|
||||
// logarithmic unit size in MediaUnits: unit_size_bytes = 0x200 * 2^unit_size_log2
|
||||
(uint8_t, unit_size_log2),
|
||||
|
||||
// 1 = fixed encryption key, 2 = don't mount the romfs, 4 = no content encryption, 0x20 = use 9.6.0 keyY generator
|
||||
(uint8_t, flags),
|
||||
|
||||
(MediaUnit32, plain_data_offset), // Unencrypted plain data
|
||||
(MediaUnit32, plain_data_size),
|
||||
(MediaUnit32, logo_offset), // logo (introduced in 5.0.0, it seems?)
|
||||
(MediaUnit32, logo_size),
|
||||
(MediaUnit32, exefs_offset), // offset from the start of this file
|
||||
(MediaUnit32, exefs_size),
|
||||
(MediaUnit32, exefs_hash_message_size),
|
||||
(std::array<uint8_t, 0x4>, unknown4),
|
||||
|
||||
(MediaUnit32, romfs_offset), // offset from the start of this file
|
||||
(MediaUnit32, romfs_size),
|
||||
(MediaUnit32, romfs_hash_message_size),
|
||||
|
||||
(std::array<uint8_t, 0x4>, unknown5),
|
||||
|
||||
(std::array<uint8_t, 0x20>, exefs_sha256),
|
||||
(std::array<uint8_t, 0x20>, romfs_sha256)
|
||||
);
|
||||
|
||||
struct Tags : expected_size_tag<0x200> {};
|
||||
|
||||
// Followed by:
|
||||
// ExHeader (optional?); 3dbrew erratum: it seems the "access descriptor" that was documented to follow the exheader is already part of this exheader
|
||||
// plaintext binary region (optional, CXI only)
|
||||
// plaintext logo region (optional, CXI only)
|
||||
// ExeFS (optional)
|
||||
// RomFS (optional)
|
||||
};
|
||||
|
||||
// Extended Header
|
||||
struct ExHeader {
|
||||
struct CodeSetInfo {
|
||||
BOOST_HANA_DEFINE_STRUCT(CodeSetInfo,
|
||||
(uint32_t, address),
|
||||
(uint32_t, size_pages),
|
||||
(uint32_t, size_bytes)
|
||||
);
|
||||
};
|
||||
|
||||
struct Flags {
|
||||
BOOST_HANA_DEFINE_STRUCT(Flags,
|
||||
(uint8_t, storage)
|
||||
);
|
||||
|
||||
using Fields = v2::BitField::Fields<uint8_t>;
|
||||
|
||||
/// If set, the ExeFS:/.code section is compressed with a LZ77 variant
|
||||
auto compress_exefs_code() const { return Fields::MakeOn<0, 1>(this); }
|
||||
};
|
||||
|
||||
/// Flags for AccessControlInfo
|
||||
struct ACIFlags {
|
||||
BOOST_HANA_DEFINE_STRUCT(ACIFlags,
|
||||
(uint32_t, storage)
|
||||
);
|
||||
|
||||
using Fields = v2::BitField::Fields<uint32_t>;
|
||||
|
||||
/**
|
||||
* @note flag1 bits in the primary ACI may be set only if the secondary ACI has them set
|
||||
*/
|
||||
auto flag1() const { return Fields::MakeOn<0, 8>(this); }
|
||||
|
||||
auto flag2() const { return Fields::MakeOn<8, 8>(this); }
|
||||
auto flag0() const { return Fields::MakeOn<16, 8>(this); }
|
||||
|
||||
auto ideal_processor() const { return Fields::MakeOn<16, 2>(this); }
|
||||
|
||||
/**
|
||||
* Thread priority
|
||||
* @todo Figure out whether this is the "default" thread priority
|
||||
* or just the priority of the main thread
|
||||
*/
|
||||
auto priority() const { return Fields::MakeOn<24, 8>(this); }
|
||||
};
|
||||
|
||||
/**
|
||||
* Descriptor for some kernel capability: There are a number of different
|
||||
* descriptor types, each of which has their own structure and fields.
|
||||
* The descriptor type is determined by the number of leading ones in
|
||||
* the bit pattern of the raw descriptor value.
|
||||
*
|
||||
* We expose this structure using a visitor pattern: The visit() method
|
||||
* checks the descriptor type according to the raw storage value,
|
||||
* constructs an appropriate strongly-typed descriptor (see children of the
|
||||
* DescriptorTypeBase structure), and calls (an appropriate overload of)
|
||||
* the given function object with this descriptor.
|
||||
*/
|
||||
struct ARM11KernelCapabilityDescriptor {
|
||||
BOOST_HANA_DEFINE_STRUCT(ARM11KernelCapabilityDescriptor,
|
||||
(uint32_t, storage)
|
||||
);
|
||||
|
||||
using Fields = v2::BitField::Fields<uint32_t>;
|
||||
|
||||
template<typename CRTP, size_t IdentifierPos>
|
||||
struct DescriptorTypeBase {
|
||||
uint32_t storage;
|
||||
|
||||
static_assert(IdentifierPos < 32, "Invalid identifying field position: Must be smaller than 32");
|
||||
|
||||
constexpr auto id_field() const { return BitField::v3::MakeFieldOn<IdentifierPos, 32 - IdentifierPos>(static_cast<const CRTP*>(this)); }
|
||||
static constexpr auto id_field_ref() { return CRTP{}.id_field().MaxValue() - 1; }
|
||||
|
||||
static CRTP Make() {
|
||||
return CRTP{}.id_field().Set(id_field_ref());
|
||||
}
|
||||
|
||||
operator ARM11KernelCapabilityDescriptor() const {
|
||||
return { storage };
|
||||
}
|
||||
};
|
||||
|
||||
/// A group of these make up a mask of SVCs that are accessible to the application
|
||||
struct SVCMaskTableEntry : DescriptorTypeBase<SVCMaskTableEntry, 27> {
|
||||
auto entry_index() const { return Fields::MakeOn<24, 3>(this); }
|
||||
auto mask() const { return Fields::MakeOn<0, 24>(this); }
|
||||
};
|
||||
|
||||
struct HandleTableInfo : DescriptorTypeBase<HandleTableInfo, 24> {
|
||||
// TODO: Verify this is given in terms of handles rather than bytes
|
||||
auto num_handles() const { return Fields::MakeOn<0, 19>(this); }
|
||||
};
|
||||
|
||||
struct MappedMemoryPage : DescriptorTypeBase<MappedMemoryPage, 20> {
|
||||
auto page_index() const { return Fields::MakeOn<0, 20>(this); }
|
||||
};
|
||||
|
||||
struct MappedMemoryRange : DescriptorTypeBase<MappedMemoryRange, 22> {
|
||||
// TODO: What about bit 21 within storage? Does it need always need to be 0? (it does according to 3dbrew!)
|
||||
|
||||
auto page_index() const { return Fields::MakeOn<0, 20>(this); }
|
||||
|
||||
// TODO: Verify this field...
|
||||
auto read_only() const { return Fields::MakeOn<20, 1>(this); }
|
||||
|
||||
};
|
||||
|
||||
struct KernelFlags : DescriptorTypeBase<KernelFlags, 23> {
|
||||
/// Allow this application to be debugged (TODO: Verify)
|
||||
auto allow_debug() const { return Fields::MakeOn<0, 1>(this); }
|
||||
|
||||
/// Force other applications to be debugged even if they don't have "allow_debug" set (TODO: Verify)
|
||||
auto force_debug() const { return Fields::MakeOn<1, 1>(this); }
|
||||
|
||||
/// "Allow non-alphanum" according to 3dbrew?
|
||||
auto unknown2() const { return Fields::MakeOn<2, 1>(this); }
|
||||
|
||||
/// If set, the shared page will be mapped with write-permissions into this process
|
||||
auto shared_page_writeable() const { return Fields::MakeOn<3, 1>(this); }
|
||||
|
||||
/// "privilege priority" according to 3dbrew?
|
||||
auto unknown4() const { return Fields::MakeOn<4, 1>(this); }
|
||||
|
||||
/// "allow main args" according to 3dbrew?
|
||||
auto unknown5() const { return Fields::MakeOn<5, 1>(this); }
|
||||
|
||||
/// "shared device memory" according to 3dbrew?
|
||||
auto unknown6() const { return Fields::MakeOn<6, 1>(this); }
|
||||
|
||||
/// "Runnable on sleep" according to 3dbrew?
|
||||
auto unknown7() const { return Fields::MakeOn<7, 1>(this); }
|
||||
|
||||
/**
|
||||
* System/Memory/Base
|
||||
* @todo Verify that this does really occupy 4 bits
|
||||
* @todo Use the kernel MemoryType enum for this
|
||||
*/
|
||||
auto memory_type() const { return Fields::MakeOn<8, 4>(this); }
|
||||
|
||||
/**
|
||||
* If set, the application code is mapped to a virtual memory
|
||||
* address specified in the exheader. Otherwise, the standard
|
||||
* starting address 0x00100000 is used.
|
||||
*/
|
||||
auto nonstandard_code_address() const { return Fields::MakeOn<12, 1>(this); }
|
||||
|
||||
/// Enable access to the second CPU core (New3DS only)
|
||||
auto enable_second_cpu_core() const { return Fields::MakeOn<13, 1>(this); }
|
||||
};
|
||||
|
||||
template<typename F>
|
||||
struct CheckDescriptorType {
|
||||
uint32_t descriptor;
|
||||
F& visitor;
|
||||
|
||||
/// Set to true if we found at least one match
|
||||
bool match_found = false;
|
||||
|
||||
template<typename T>
|
||||
void operator()(boost::mp11::mp_identity<T>) {
|
||||
T typed_descriptor = { descriptor };
|
||||
|
||||
auto actual_field = typed_descriptor.id_field()();
|
||||
if (actual_field != typed_descriptor.id_field_ref())
|
||||
return;
|
||||
|
||||
match_found = true;
|
||||
|
||||
T ret;
|
||||
ret.storage = descriptor;
|
||||
visitor(ret);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines the type of this descriptor, constructs a strongly-typed
|
||||
* variant of it (see children of DescriptorTypeBase), and calls the
|
||||
* given visitor with it.
|
||||
*
|
||||
* If no known strongly-typed descriptor exists (or the raw descriptor
|
||||
* is just an invalid descriptor in the first place), the visitor is
|
||||
* called with the raw uint32_t descriptor value.
|
||||
*/
|
||||
template<typename F>
|
||||
auto visit(F&& visitor) {
|
||||
using namespace boost::mp11;
|
||||
|
||||
// TODO: Order these by increasing number of leading zeroes
|
||||
using all_types = mp_transform<mp_identity, mp_list<SVCMaskTableEntry, HandleTableInfo, MappedMemoryPage, MappedMemoryRange, KernelFlags>>;
|
||||
|
||||
auto checker = CheckDescriptorType<F>{storage, visitor};
|
||||
auto checker2 = boost::mp11::mp_for_each<all_types>(checker);
|
||||
if (!checker2.match_found)
|
||||
return visitor(storage);
|
||||
}
|
||||
};
|
||||
|
||||
struct AccessControlInfo {
|
||||
BOOST_HANA_DEFINE_STRUCT(AccessControlInfo,
|
||||
// "ARM11 Local System Capabilities"
|
||||
(uint64_t, program_id),
|
||||
|
||||
// Checked against the lower word in the FIRM title id?
|
||||
(uint32_t, version),
|
||||
|
||||
(ACIFlags, flags),
|
||||
|
||||
// Resource limit descriptors + storage info
|
||||
(std::array<uint8_t, 0x40>, unknown5),
|
||||
(std::array<std::array<uint8_t, 8>, 0x20>, service_access_list),
|
||||
(std::array<uint8_t, 0x20>, unknown6),
|
||||
|
||||
(std::array<ARM11KernelCapabilityDescriptor, 28>, arm11_kernel_capabilities),
|
||||
|
||||
// "ARM9 access control"
|
||||
(std::array<uint8_t, 0x20>, unknown7)
|
||||
);
|
||||
|
||||
struct Tags : expected_size_tag<0x200> {};
|
||||
};
|
||||
|
||||
BOOST_HANA_DEFINE_STRUCT(ExHeader,
|
||||
// Begin of "System Control Info"
|
||||
(std::array<unsigned char, 8>, application_title),
|
||||
|
||||
(std::array<unsigned char, 5>, unknown),
|
||||
|
||||
(Flags, flags),
|
||||
|
||||
(uint16_t, remaster_version), // ???
|
||||
|
||||
(CodeSetInfo, section_text),
|
||||
(uint32_t, stack_size),
|
||||
(CodeSetInfo, section_ro),
|
||||
(std::array<uint8_t, 0x4>, unknown3),
|
||||
// data section information is exclusive of bss
|
||||
(CodeSetInfo, section_data),
|
||||
(uint32_t, bss_size),
|
||||
|
||||
// Title IDs of dependencies (0 if entry unused)
|
||||
(std::array<uint64_t, 0x30>, dependencies),
|
||||
|
||||
(uint64_t, save_data_size),
|
||||
|
||||
(uint64_t, jump_id), // Seems to be some sort of title id???
|
||||
(std::array<uint8_t, 0x30>, unknown4),
|
||||
// End of "System Control Info"
|
||||
|
||||
/**
|
||||
* Primary ACI. Specifies the actual parameters that are used for
|
||||
* access control, while the secondary ACI (see below) restricts the
|
||||
* set of valid parameters specified in the primary one. This scheme
|
||||
* allows game developers to choose what privileges to give to their
|
||||
* applications while keeping the console vendor in control over the
|
||||
* maximum set of privileges they can possibly have.
|
||||
*/
|
||||
(AccessControlInfo, aci),
|
||||
|
||||
/// RSA2048-SHA256 signature covering ncch_sig_pub_key and aci_limits
|
||||
(std::array<uint8_t, 0x100>, subsignature),
|
||||
|
||||
/// Public key for use in NCCH RSA2048-SHA256 signatures
|
||||
(std::array<uint8_t, 0x100>, ncch_sig_pub_key),
|
||||
|
||||
/**
|
||||
* The secondary ACI, signed by Nintendo (signature given above) as a
|
||||
* limitation to the first ACI; if the first ACI does not match the
|
||||
* contents of the second, the system will refuse to boot this title.
|
||||
*
|
||||
* For retail applications, there is not much point in this since the
|
||||
* entity who signed the ExHeader will likely be the same as the one
|
||||
* who signed the second ACI. However, for debug unit builds (where
|
||||
* game developers can self-sign their titles), the second ACI can
|
||||
* not be changed without breaking the signature, hence it keeps
|
||||
* developers from requesting privileges for the application that
|
||||
* they should not be using on retail units.
|
||||
*
|
||||
* @todo Figure out and document what specifically "limitation" means here
|
||||
*/
|
||||
(AccessControlInfo, aci_limits)
|
||||
);
|
||||
|
||||
struct Tags : expected_size_tag<0x800> {};
|
||||
};
|
||||
|
||||
struct ExeFSHeader {
|
||||
struct FileHeader {
|
||||
BOOST_HANA_DEFINE_STRUCT(FileHeader,
|
||||
(std::array<uint8_t, 8>, name), // ".code", "logo", "banner", or "icon"
|
||||
(uint32_t, offset), // byte offset starting after the ExeFS header
|
||||
(uint32_t, size_bytes)
|
||||
);
|
||||
};
|
||||
|
||||
BOOST_HANA_DEFINE_STRUCT(ExeFSHeader,
|
||||
// TODO: Confirm that 10 files is indeed the maximum
|
||||
(std::array<FileHeader, 10>, files),
|
||||
(std::array<uint8_t, 0x20>, unknown),
|
||||
// Hashes for each ExeFS file; stored in reverse order, i.e. the last hashes element corresponds to the first file
|
||||
(std::array<std::array<uint8_t, 0x20>, 10>, hashes)
|
||||
);
|
||||
|
||||
uint64_t GetExeFSSize() const {
|
||||
using ranges::v3::max;
|
||||
using ranges::v3::view::transform;
|
||||
auto end_offset = [](auto& file) { return file.offset + file.size_bytes; };
|
||||
|
||||
// TODO: Don't hardcode the header size!
|
||||
return 0x200 + max(files | transform(end_offset));
|
||||
}
|
||||
|
||||
struct Tags : expected_size_tag<0x200> {};
|
||||
|
||||
static constexpr const char code_section_name[8] = ".code\0\0";
|
||||
};
|
||||
|
||||
struct RomFSLevel3Header {
|
||||
BOOST_HANA_DEFINE_STRUCT(RomFSLevel3Header,
|
||||
(uint32_t, header_size), // in bytes
|
||||
// Offsets and sizes are measured in bytes.
|
||||
// Offsets are all from start of this header.
|
||||
(uint32_t, dir_hash_offset),
|
||||
(uint32_t, dir_hash_size),
|
||||
(uint32_t, dir_metadata_offset),
|
||||
(uint32_t, dir_metadata_size),
|
||||
(uint32_t, file_hash_offset),
|
||||
(uint32_t, file_hash_size),
|
||||
(uint32_t, file_metadata_offset),
|
||||
(uint32_t, file_metadata_size),
|
||||
(uint32_t, file_data_offset)
|
||||
);
|
||||
|
||||
struct Tags : expected_size_tag<0x28> {};
|
||||
};
|
||||
|
||||
struct RomFSFileMetadata {
|
||||
BOOST_HANA_DEFINE_STRUCT(RomFSFileMetadata,
|
||||
// Offsets and sizes are all measured in bytes.
|
||||
(uint32_t, parent_dir_index), // index within directory metadata table
|
||||
(uint32_t, unknown2),
|
||||
(uint64_t, data_offset), // from start of file data section
|
||||
(uint64_t, data_size),
|
||||
(uint32_t, unknown5),
|
||||
(uint32_t, name_size) // length of name in bytes (NOTE: Actual filename data size is aligned to 4 bytes)
|
||||
);
|
||||
|
||||
struct Tags : expected_size_tag<0x20> {};
|
||||
};
|
||||
|
||||
struct NCSDHeader {
|
||||
struct Partition {
|
||||
BOOST_HANA_DEFINE_STRUCT(Partition,
|
||||
(MediaUnit32, offset), // from start of file
|
||||
(MediaUnit32, size)
|
||||
);
|
||||
};
|
||||
|
||||
BOOST_HANA_DEFINE_STRUCT(NCSDHeader,
|
||||
(std::array<uint8_t, 0x100>, signature), // RSA-2048 SHA-256 signature of this header (starting from magic?)
|
||||
(std::array<uint8_t, 4>, magic), // always "NCSD"
|
||||
(MediaUnit32, size), // total NCSD size
|
||||
(std::array<uint8_t, 0x18>, unknown),
|
||||
(std::array<Partition, 8>, partition_table),
|
||||
(std::array<uint8_t, 0xa0>, unknown2)
|
||||
);
|
||||
|
||||
struct Tags : expected_size_tag<0x200> {};
|
||||
};
|
||||
|
||||
} // namespace FileFormat
|
580
source/platform/fs.hpp
Normal file
580
source/platform/fs.hpp
Normal file
|
@ -0,0 +1,580 @@
|
|||
#pragma once
|
||||
|
||||
#include "ipc.hpp"
|
||||
|
||||
// Include definitions for ProgramInfo and ProgramHandle
|
||||
#include "pxi.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <boost/hana/define_struct.hpp>
|
||||
|
||||
namespace Platform {
|
||||
|
||||
namespace FS {
|
||||
|
||||
using ProgramInfo = PXI::PM::ProgramInfo;
|
||||
|
||||
struct StorageInfo {
|
||||
uint32_t unknown1;
|
||||
uint32_t unknown2;
|
||||
uint32_t unknown3;
|
||||
uint32_t unknown4;
|
||||
uint32_t unknown5;
|
||||
uint32_t unknown6;
|
||||
uint32_t unknown7;
|
||||
uint32_t unknown8;
|
||||
|
||||
static auto IPCDeserialize(uint32_t a, uint32_t b, uint32_t c, uint32_t d, uint32_t e, uint32_t f, uint32_t g, uint32_t h) {
|
||||
return StorageInfo { a, b, c, d, e, f, g, h };
|
||||
}
|
||||
static auto IPCSerialize(const StorageInfo& data) {
|
||||
return std::make_tuple(data.unknown1, data.unknown2, data.unknown3, data.unknown4, data.unknown5, data.unknown6, data.unknown7, data.unknown8);
|
||||
}
|
||||
|
||||
using ipc_data_size = std::integral_constant<size_t, 8>;
|
||||
};
|
||||
|
||||
enum class MediaType : uint8_t {
|
||||
NAND = 0,
|
||||
SD = 1,
|
||||
GameCard = 2,
|
||||
};
|
||||
|
||||
struct ExtSaveDataInfo {
|
||||
MediaType media_type;
|
||||
uint8_t unknown[3];
|
||||
uint64_t save_id;
|
||||
uint32_t unknown2;
|
||||
|
||||
static auto IPCDeserialize(uint32_t a, uint32_t b, uint32_t c, uint32_t d) {
|
||||
return ExtSaveDataInfo {
|
||||
static_cast<MediaType>(a & 0xff),
|
||||
{ static_cast<uint8_t>(a >> 8), static_cast<uint8_t>(a >> 16), static_cast<uint8_t>(a >> 24) },
|
||||
(uint64_t { c } << 32) | b,
|
||||
d };
|
||||
}
|
||||
|
||||
static auto IPCSerialize(const ExtSaveDataInfo& data) {
|
||||
return std::make_tuple(static_cast<uint32_t>(data.media_type) | (uint32_t { data.unknown[0] } << 8) | (uint32_t { data.unknown[1] } << 16) | (uint32_t { data.unknown[2] } << 24),
|
||||
static_cast<uint32_t>(data.save_id), static_cast<uint32_t>(data.save_id >> 32), data.unknown2);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Parameters used when the archive was initially formatted
|
||||
*/
|
||||
struct ArchiveFormatInfo {
|
||||
uint32_t max_size;
|
||||
uint32_t max_directories;
|
||||
uint32_t max_files;
|
||||
bool duplicate_data;
|
||||
|
||||
static auto IPCDeserialize(uint32_t a, uint32_t b, uint32_t c, uint32_t d) {
|
||||
// TODO: Some games (e.g. Cubic Ninja during startup) use values like
|
||||
// 0x0ffffe01 for this. Test if only the lowest byte is
|
||||
// considered here, or if the entire word is compared against 0
|
||||
// instead
|
||||
if ((d & 0xff) > 1) {
|
||||
throw std::runtime_error("Invalid input value for boolean field");
|
||||
}
|
||||
|
||||
return ArchiveFormatInfo { a, b, c, static_cast<bool>(d & 0xff) };
|
||||
}
|
||||
|
||||
static auto IPCSerialize(const ArchiveFormatInfo& data) noexcept {
|
||||
return std::make_tuple(data.max_size, data.max_directories, data.max_files, static_cast<uint32_t>(data.duplicate_data));
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Turn these into strong typedefs
|
||||
using ArchiveId = uint32_t;
|
||||
using ArchiveHandle = uint64_t; // FS-internal handle for opened archives
|
||||
|
||||
namespace IPC = Platform::IPC;
|
||||
|
||||
/// fs:USER and fs:LDR service commands
|
||||
namespace User {
|
||||
|
||||
/**
|
||||
* Observed error codes:
|
||||
* - 0xc8804465: Didn't register current process to fs:REG ?
|
||||
*/
|
||||
using Initialize = IPC::IPCCommand<0x801>::add_process_id
|
||||
::response;
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Transaction
|
||||
* - Archive handle
|
||||
* - File path type
|
||||
* - File path size
|
||||
* - File access mode (bit 0 = read, bit 1 = write, bit 2 = create)
|
||||
* - Attributes
|
||||
* - File path
|
||||
*
|
||||
* Error codes:
|
||||
* - 0xe0c046f8: Trying to open files with unsupported flags (e.g. 0, 4, and 5 on archive id 8)
|
||||
* - 0xc92044e6: Invalid combination of file access mode flags (e.g. 7)
|
||||
* Trying to open a file that is already opened
|
||||
* - 0xe0e046be: Trying to open a file from an invalid path (e.g. empty, no leading "/", etc)
|
||||
* - 0xe0c04702: Trying to escape the root directory via .. (e.g. "/../test") <- TODO: Could indicate ArchiveSaveData just doesn't support ".." at all!
|
||||
*/
|
||||
using OpenFile = IPC::IPCCommand<0x802>::add_uint32::add_uint64::add_uint32::add_uint32::add_uint32::add_uint32::add_static_buffer/*TODO: Id*/
|
||||
::response::add_and_close_handle<IPC::HandleType::ClientSession>;
|
||||
struct OpenFileDirectly : IPC::IPCCommand<0x803>::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_static_buffer::add_static_buffer
|
||||
::response::add_and_close_handle<IPC::HandleType::ClientSession> {};
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Transaction
|
||||
* - Archive handle
|
||||
* - File path type
|
||||
* - File path size
|
||||
* - File path
|
||||
*
|
||||
* Error codes:
|
||||
* - 0xc8804470: File does not exist
|
||||
* - 0xd9001830: Invalid arguments
|
||||
*/
|
||||
using DeleteFile = IPC::IPCCommand<0x804>::add_uint32::add_uint64::add_uint32::add_uint32::add_static_buffer
|
||||
::response;
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Transaction
|
||||
* - Source archive handle
|
||||
* - Source file path type
|
||||
* - Source file path size
|
||||
* - Target archive handle
|
||||
* - Target file path type
|
||||
* - Target file path size
|
||||
* - Source file path
|
||||
* - Target file path
|
||||
*/
|
||||
using RenameFile = IPC::IPCCommand<0x805>::add_uint32::add_uint64::add_uint32::add_uint32::add_uint64::add_uint32::add_uint32::add_static_buffer::add_static_buffer
|
||||
::response;
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Transaction
|
||||
* - Archive handle
|
||||
* - Directory path type
|
||||
* - Directory path size
|
||||
* - Directory path
|
||||
*
|
||||
* Error codes:
|
||||
* - 0xc8804471: Directory does not exist
|
||||
* - 0xc92044e6: Directory is still opened
|
||||
* - 0xc92044f0: Directory is not empty
|
||||
*/
|
||||
using DeleteDirectory = IPC::IPCCommand<0x806>::add_uint32::add_uint64::add_uint32::add_uint32::add_static_buffer
|
||||
::response;
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Transaction
|
||||
* - Archive handle
|
||||
* - Directory path type
|
||||
* - Directory path size
|
||||
* - Directory path
|
||||
*
|
||||
* Error codes:
|
||||
* - 0xc8804471: Directory does not exist
|
||||
*/
|
||||
using DeleteDirectoryRecursively = IPC::IPCCommand<0x807>::add_uint32::add_uint64::add_uint32::add_uint32::add_static_buffer
|
||||
::response;
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Transaction
|
||||
* - Archive handle
|
||||
* - File path type
|
||||
* - File path size
|
||||
* - Attributes
|
||||
* - Initial file size (TODOTEST: will it be zero-filled?)
|
||||
* - File path buffer (id 0)
|
||||
*
|
||||
* Error codes:
|
||||
* - 0xc82044b4: No effect, e.g. because the file already existed
|
||||
* - 0xc8804471: Directory does not exist
|
||||
* - 0xe0c046f8: Unsupported operation (e.g. flag combination or empty file size)
|
||||
*/
|
||||
using CreateFile = IPC::IPCCommand<0x808>::add_uint32::add_uint64::add_uint32::add_uint32::add_uint32::add_uint64::add_static_buffer
|
||||
::response;
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Transaction
|
||||
* - Archive handle
|
||||
* - File path type
|
||||
* - File path size
|
||||
* - Attributes
|
||||
* - File path buffer (id 0)
|
||||
*
|
||||
* Error codes:
|
||||
* - 0xc82044b9: Path already exists (whether it points to a directory or not)
|
||||
* - 0xe0e046be: Path too long (used instead of 0xe0e046bf by shared extdata archive 0x7)
|
||||
* - 0xe0e046bf: Path too long (used by most archives instead of 0xe0e046be)
|
||||
*/
|
||||
using CreateDirectory = IPC::IPCCommand<0x809>::add_uint32::add_uint64::add_uint32::add_uint32::add_uint32::add_static_buffer
|
||||
::response;
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Archive handle
|
||||
* - Directory path type
|
||||
* - Directory path size
|
||||
* - Directory path
|
||||
*
|
||||
* Error codes:
|
||||
* - 0xc8804471: Directory does not exist
|
||||
*/
|
||||
using OpenDirectory = IPC::IPCCommand<0x80b>::add_uint64::add_uint32::add_uint32::add_static_buffer/*TODO: Id*/
|
||||
::response::add_and_close_handle<IPC::HandleType::ClientSession>;
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Archive id
|
||||
* - Path type
|
||||
* - Path size (including null-terminator)
|
||||
*
|
||||
* Outputs:
|
||||
* - Archive handle
|
||||
*
|
||||
* Error codes:
|
||||
* - 0xc8804470: Trying to open an archive path that doesn't exist. Depending on the archive id, you may need to call e.g. CreateSystemSaveData first
|
||||
* - 0xc8804478: Observed while trying to open a savegame with size 0
|
||||
* - 0xc8a04555: Observed while trying to open a corrupted savegame via archive id 4 (needed to call FormatSaveData first)
|
||||
*/
|
||||
using OpenArchive = IPC::IPCCommand<0x80c>::add_uint32::add_uint32::add_uint32::add_static_buffer
|
||||
::response::add_uint64;
|
||||
|
||||
/**
|
||||
* Contents of the input and output buffers depend on the given action.
|
||||
*
|
||||
* Inputs:
|
||||
* - Archive handle
|
||||
* - Action
|
||||
* - Input data size
|
||||
* - Output data size
|
||||
* - Input buffer
|
||||
* - Output buffer
|
||||
*/
|
||||
using ControlArchive = IPC::IPCCommand<0x80d>::add_uint64::add_uint32::add_uint32::add_uint32::add_buffer_mapping_read::add_buffer_mapping_write
|
||||
::response::add_buffer_mapping_read::add_buffer_mapping_write;
|
||||
|
||||
using CloseArchive = IPC::IPCCommand<0x80e>::add_uint64
|
||||
::response;
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Save data block size
|
||||
* - Number of directories
|
||||
* - Number of files
|
||||
* - Number of directory buckets
|
||||
* - Number of file buckets
|
||||
* - Unknown
|
||||
*/
|
||||
using FormatOwnSaveData = IPC::IPCCommand<0x80f>::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32
|
||||
::response;
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Save id
|
||||
* - Total size
|
||||
* - Block size (usually 0x1000)
|
||||
* - Number of directories
|
||||
* - Number of files
|
||||
* - Unknown
|
||||
* - Unknown
|
||||
* - Unknown
|
||||
*/
|
||||
using CreateSystemSaveDataLegacy = IPC::IPCCommand<0x810>::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32
|
||||
::response;
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Save id
|
||||
*/
|
||||
using DeleteSystemSaveDataLegacy = IPC::IPCCommand<0x811>::add_uint32
|
||||
::response;
|
||||
|
||||
|
||||
/**
|
||||
* Outputs:
|
||||
* - 0 if CTR card, 1 if TWL card
|
||||
*/
|
||||
using GetCardType = IPC::IPCCommand<0x813>
|
||||
::response::add_uint32;
|
||||
|
||||
using IsSdmcDetected = IPC::IPCCommand<0x817>
|
||||
::response::add_uint32;
|
||||
|
||||
struct IsSdmcWritable : IPC::IPCCommand<0x818>
|
||||
::response::add_uint32 {};
|
||||
/*
|
||||
* Returns the ProgramInfo data that was given to fs:Reg::Register for the input process id
|
||||
*/
|
||||
using GetProgramLaunchInfo = IPC::IPCCommand<0x82f>::add_uint32
|
||||
::response::add_serialized<PXI::PM::ProgramInfo>;
|
||||
/**
|
||||
* Wraps around CreateExtSaveData. Note that the parameter order is slightly
|
||||
* shuffled, and not all members of ExtSaveDataInfo are specified explicitly.
|
||||
*
|
||||
* Inputs:
|
||||
* - Media type
|
||||
* - Save id
|
||||
* - SMDH size
|
||||
* - Number of directories
|
||||
* - Number of files
|
||||
* - SMDH data
|
||||
*/
|
||||
using CreateExtSaveDataLegacy = IPC::IPCCommand<0x830>::add_uint32::add_uint64::add_uint32::add_uint32::add_uint32::add_buffer_mapping_read
|
||||
::response::add_buffer_mapping_read;
|
||||
|
||||
/**
|
||||
* Like DeleteExtSaveData, but only specified MediaType and lower 32 bits of save id as inputs
|
||||
*/
|
||||
using DeleteExtSaveDataLegacy = IPC::IPCCommand<0x835>::add_uint32::add_uint32
|
||||
::response;
|
||||
|
||||
using Unknown0x839 = IPC::IPCCommand<0x839>
|
||||
::response;
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Archive ID
|
||||
* - Path type, size, and buffer
|
||||
*
|
||||
* Outputs:
|
||||
* - Total size
|
||||
* - Number of directories
|
||||
* - Number of files
|
||||
*/
|
||||
using GetFormatInfo = IPC::IPCCommand<0x845>::add_uint32::add_uint32::add_uint32::add_static_buffer
|
||||
::response::add_serialized<ArchiveFormatInfo>;
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Archive ID
|
||||
* - Path type and size
|
||||
* - Number of save data blocks
|
||||
* - Number of directories
|
||||
* - Number of files
|
||||
* - Number of directory buckets
|
||||
* - Number of file buckets
|
||||
* - Unknown
|
||||
*
|
||||
* Error codes:
|
||||
* - 0xe0e046be: Trying to format archive with id different than 0x4 and 0x567890b2
|
||||
*/
|
||||
using FormatSaveData = IPC::IPCCommand<0x84c>::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_static_buffer
|
||||
::response;
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - 8 words of input hashes (unused?)
|
||||
* - Input data size in bytes
|
||||
* - 4 words for flags
|
||||
* - Input data buffer
|
||||
*/
|
||||
using UpdateSha256Context = IPC::IPCCommand<0x84e>::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_buffer_mapping_read
|
||||
::response::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_buffer_mapping_read;
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Extdata info
|
||||
* - Number of directories
|
||||
* - Number of files
|
||||
* - Size limit (may be -1)
|
||||
* - SMDH size
|
||||
* - SMDH data
|
||||
*/
|
||||
using CreateExtSaveData = IPC::IPCCommand<0x851>::add_serialized<ExtSaveDataInfo>::add_uint32::add_uint32::add_uint64::add_uint32::add_buffer_mapping_read
|
||||
::response::add_buffer_mapping_read;
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Extdata info
|
||||
*
|
||||
* Error codes:
|
||||
* - 0xc8804478: Not found
|
||||
* - 0xc92044fa: Attempted to delete archive while it was still opened
|
||||
*/
|
||||
using DeleteExtSaveData = IPC::IPCCommand<0x852>::add_serialized<ExtSaveDataInfo>
|
||||
::response;
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Media type (and other things?)
|
||||
* - Save id
|
||||
* - Total size
|
||||
* - Block size (usually 0x1000)
|
||||
* - Number of directories
|
||||
* - Number of files
|
||||
* - Unknown
|
||||
* - Unknown
|
||||
* - Unknown
|
||||
*
|
||||
* Error codes:
|
||||
* - 0xc86044c8, observed with total_size=0x1000
|
||||
* - 0xc92044e7
|
||||
*/
|
||||
using CreateSystemSaveData = IPC::IPCCommand<0x856>::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32::add_uint32
|
||||
::response;
|
||||
using DeleteSystemSaveData = IPC::IPCCommand<0x857>::add_uint32::add_uint32
|
||||
::response;
|
||||
|
||||
/**
|
||||
* Known SDK versions:
|
||||
* - 0xa0001c8 (4.5 cfg module)
|
||||
* - 0xb0502c8 (11.x cfg module)
|
||||
* - ...
|
||||
*/
|
||||
using InitializeWithSdkVersion = IPC::IPCCommand<0x861>::add_uint32::add_process_id
|
||||
::response;
|
||||
using GetPriority = IPC::IPCCommand<0x863>
|
||||
::response::add_uint32;
|
||||
|
||||
} // namespace User
|
||||
|
||||
/// fs:REG service commands
|
||||
namespace Reg {
|
||||
|
||||
/**
|
||||
* Authorizes the process with the given ProcessId to use the FS services.
|
||||
* The input ProgramHandle must previously have been registered through LoadProgram.
|
||||
*
|
||||
* Inputs:
|
||||
* - Process id
|
||||
* - PXIPM ProgramHandle
|
||||
* - ProgramInfo
|
||||
* - StorageInfo
|
||||
*/
|
||||
using Register = IPC::IPCCommand<0x401>
|
||||
::add_uint32::add_serialized<PXI::PM::ProgramHandle>
|
||||
::add_serialized<ProgramInfo>
|
||||
::add_serialized<StorageInfo>
|
||||
::response;
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - Process id
|
||||
*/
|
||||
using Unregister = IPC::IPCCommand<0x402>
|
||||
::add_uint32
|
||||
::response;
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - ProgramInfo
|
||||
*
|
||||
* Outputs:
|
||||
* - Program handle
|
||||
*/
|
||||
using LoadProgram = IPC::IPCCommand<0x404>
|
||||
::add_serialized<PXI::PM::ProgramInfo>
|
||||
::response::add_serialized<PXI::PM::ProgramHandle>;
|
||||
|
||||
/**
|
||||
* Inputs:
|
||||
* - PXI Program handle
|
||||
* TODO: This actually seems to be a title id!
|
||||
*/
|
||||
using CheckHostLoadId = IPC::IPCCommand<0x406>
|
||||
::add_serialized<PXI::PM::ProgramHandle>
|
||||
::response; // TODO: Response unverified
|
||||
|
||||
} // namespace Reg
|
||||
|
||||
/// File session commands
|
||||
namespace File {
|
||||
|
||||
using Control = IPC::IPCCommand<0x401>::add_uint32::add_uint32::add_uint32
|
||||
::add_buffer_mapping_read
|
||||
::add_buffer_mapping_write
|
||||
::response;
|
||||
|
||||
using OpenSubFile = IPC::IPCCommand<0x801>::add_uint64::add_uint64
|
||||
::response::add_and_close_handle<IPC::HandleType::ClientSession>;
|
||||
|
||||
/**
|
||||
* Error codes:
|
||||
* - 0xc92044e6: Trying to read from a write-only file
|
||||
* - 0xc8a044dc: Trying to access a file that was closed
|
||||
*/
|
||||
struct Read : IPC::IPCCommand<0x802>::add_uint64::add_uint32
|
||||
::add_buffer_mapping_write
|
||||
::response::add_uint32::add_buffer_mapping_write { };
|
||||
|
||||
/**
|
||||
* Error codes:
|
||||
* - 0xc92044e6: Trying to write to a read-only file
|
||||
* - 0xc8a044dc: Trying to access a file that was closed
|
||||
*/
|
||||
struct Write : IPC::IPCCommand<0x803>::add_uint64::add_uint32::add_uint32
|
||||
::add_buffer_mapping_read
|
||||
::response::add_uint32::add_buffer_mapping_read { };
|
||||
|
||||
/**
|
||||
* Error codes:
|
||||
* - 0xc8a044dc: Trying to access a file that was closed
|
||||
*/
|
||||
using GetSize = IPC::IPCCommand<0x804>
|
||||
::response::add_uint64;
|
||||
|
||||
/**
|
||||
* Error codes:
|
||||
* - 0xc8a044dc: Trying to access a file that was closed
|
||||
*/
|
||||
using SetSize = IPC::IPCCommand<0x805>::add_uint64
|
||||
::response;
|
||||
|
||||
using Close = IPC::IPCCommand<0x808>
|
||||
::response;
|
||||
|
||||
using Flush = IPC::IPCCommand<0x809>
|
||||
::response;
|
||||
|
||||
using OpenLinkFile = IPC::IPCCommand<0x80c>
|
||||
::response::add_and_close_handle<IPC::HandleType::ClientSession>;
|
||||
|
||||
} // namespace File
|
||||
|
||||
/// Directory session commands
|
||||
namespace Dir {
|
||||
|
||||
struct Entry {
|
||||
BOOST_HANA_DEFINE_STRUCT(Entry,
|
||||
(std::array<char16_t, 0x106>, name), // UTF-16 entry name (TODO: null terminated?)
|
||||
(std::array<uint8_t, 0x14>, unknown),
|
||||
(uint64_t, size) // file size in bytes?
|
||||
);
|
||||
};
|
||||
static_assert(sizeof(Entry) == 0x228);
|
||||
|
||||
/**
|
||||
* Iteratively reads a number of directory entries. The internal directory
|
||||
* object will track a read offset and increase it accordingly when this
|
||||
* function is called.
|
||||
*
|
||||
* Inputs:
|
||||
* - Number of entries to read
|
||||
* - Directory path type
|
||||
* - Directory path size
|
||||
* - Directory path
|
||||
*/
|
||||
using Read = IPC::IPCCommand<0x801>::add_uint32::add_buffer_mapping_write
|
||||
::response::add_uint32::add_buffer_mapping_write;
|
||||
|
||||
using Close = IPC::IPCCommand<0x802>
|
||||
::response;
|
||||
|
||||
}
|
||||
|
||||
} // namespace FS
|
||||
|
||||
} // namespace Platform
|
25
source/platform/gpio.hpp
Normal file
25
source/platform/gpio.hpp
Normal file
|
@ -0,0 +1,25 @@
|
|||
#pragma once
|
||||
|
||||
#include "ipc.hpp"
|
||||
|
||||
namespace Platform {
|
||||
|
||||
/**
|
||||
* GPIO: Interface for system processes to hardware interrupts.
|
||||
*/
|
||||
namespace GPIO {
|
||||
|
||||
// All IPC commands are common between the services in this module, hence they
|
||||
// are not put in a nested namespace
|
||||
|
||||
namespace IPC = Platform::IPC;
|
||||
|
||||
// TODO: Verify the input handle type for Unb/BindInterrupt
|
||||
using BindInterrupt = IPC::IPCCommand<0x9>::add_uint32::add_uint32::add_handle<IPC::HandleType::Event>
|
||||
::response;
|
||||
using UnbindInterrupt = IPC::IPCCommand<0xa>::add_uint32::add_handle<IPC::HandleType::Event>
|
||||
::response;
|
||||
|
||||
} // namespace GPIO
|
||||
|
||||
} // namespace Platform
|
118
source/platform/gpu/float.hpp
Normal file
118
source/platform/gpu/float.hpp
Normal file
|
@ -0,0 +1,118 @@
|
|||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Pica {
|
||||
|
||||
template<unsigned ExponentBits, unsigned MantissaBits>
|
||||
struct floatN {
|
||||
static constexpr unsigned bits = 1 + ExponentBits + MantissaBits;
|
||||
static constexpr uint32_t MantissaMask = (1 << MantissaBits) - 1;
|
||||
static constexpr uint32_t ExponentMask = ((1 << ExponentBits) - 1) << MantissaBits;
|
||||
static constexpr uint32_t SignMask = (1 << (ExponentBits + MantissaBits));
|
||||
static constexpr uint32_t FullMask = ((1 << (ExponentBits + MantissaBits + 1)) - 1);
|
||||
|
||||
static constexpr unsigned ExponentBias = (1 << (ExponentBits - 1)) - 1;
|
||||
|
||||
static floatN FromFloat32(float val) {
|
||||
floatN ret;
|
||||
ret.value = val;
|
||||
return ret;
|
||||
}
|
||||
|
||||
static floatN FromRawFloat(uint32_t hex) {
|
||||
floatN ret;
|
||||
if ((hex & FullMask) == 0) {
|
||||
ret.value = 0;
|
||||
} else {
|
||||
uint32_t mantissa = hex & MantissaMask;
|
||||
uint32_t exponent = (hex & ExponentMask) >> MantissaBits;
|
||||
uint32_t sign = hex >> (ExponentBits + MantissaBits);
|
||||
if (exponent == ExponentMask) {
|
||||
throw /*std::runtime_error*/("Can't convert infinity/NaN float value");
|
||||
} else if (exponent == 0) {
|
||||
// TODO: Actually a denormal
|
||||
ret.value = 0;
|
||||
} else {
|
||||
constexpr unsigned MantissaBits32 = 23;
|
||||
constexpr unsigned ExponentBias32 = 127;
|
||||
mantissa <<= (MantissaBits32 - MantissaBits);
|
||||
exponent += (ExponentBias32 - ExponentBias);
|
||||
exponent <<= MantissaBits32;
|
||||
uint32_t raw_value = (sign << 31u) | exponent | mantissa;
|
||||
memcpy(&ret.value, &raw_value, sizeof(raw_value));
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
float ToFloat32() const {
|
||||
return value;
|
||||
}
|
||||
|
||||
floatN operator / (const floatN& flt) const {
|
||||
return floatN::FromFloat32(ToFloat32() / flt.ToFloat32());
|
||||
}
|
||||
|
||||
floatN operator + (const floatN& flt) const {
|
||||
return floatN::FromFloat32(ToFloat32() + flt.ToFloat32());
|
||||
}
|
||||
|
||||
floatN operator - (const floatN& flt) const {
|
||||
return floatN::FromFloat32(ToFloat32() - flt.ToFloat32());
|
||||
}
|
||||
|
||||
floatN operator - () const {
|
||||
return floatN::FromFloat32(-ToFloat32());
|
||||
}
|
||||
|
||||
bool operator < (const floatN& flt) const {
|
||||
return ToFloat32() < flt.ToFloat32();
|
||||
}
|
||||
|
||||
bool operator > (const floatN& flt) const {
|
||||
return ToFloat32() > flt.ToFloat32();
|
||||
}
|
||||
|
||||
bool operator >= (const floatN& flt) const {
|
||||
return ToFloat32() >= flt.ToFloat32();
|
||||
}
|
||||
|
||||
bool operator <= (const floatN& flt) const {
|
||||
return ToFloat32() <= flt.ToFloat32();
|
||||
}
|
||||
|
||||
bool operator == (const floatN& flt) const {
|
||||
return ToFloat32() == flt.ToFloat32();
|
||||
}
|
||||
|
||||
bool operator != (const floatN& flt) const {
|
||||
return ToFloat32() != flt.ToFloat32();
|
||||
}
|
||||
|
||||
private:
|
||||
// Stored as a regular float, merely for convenience
|
||||
// TODO: Perform proper arithmetic on this!
|
||||
float value;
|
||||
};
|
||||
|
||||
using float16 = floatN<5, 10>;
|
||||
using float24 = floatN<7, 16>;
|
||||
|
||||
const float24 zero_f24 = float24::FromFloat32(0.f);
|
||||
|
||||
inline float24 operator *(const float24& left, const float24& right) {
|
||||
// NOTE: IEEE 754 requires "inf * 0 == NaN", but the 3DS shader unit yields 0 in this case
|
||||
// Effects of (not emulating) this are observed in missing UI elements in Zelda: Ocarina of Time 3D
|
||||
if (left == zero_f24 || right == zero_f24) {
|
||||
if (!std::isnan(left.ToFloat32()) && !std::isnan(right.ToFloat32())) {
|
||||
return zero_f24;
|
||||
}
|
||||
}
|
||||
return float24::FromFloat32(left.ToFloat32() * right.ToFloat32());
|
||||
}
|
||||
|
||||
} // namespace Pica
|
1329
source/platform/gpu/pica.hpp
Normal file
1329
source/platform/gpu/pica.hpp
Normal file
File diff suppressed because it is too large
Load diff
52
source/platform/gpu/zorder.hpp
Normal file
52
source/platform/gpu/zorder.hpp
Normal file
|
@ -0,0 +1,52 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Pica {
|
||||
|
||||
struct ZOrderOffset {
|
||||
static constexpr uint32_t block_width = 8;
|
||||
static constexpr uint32_t block_height = 8;
|
||||
|
||||
uint32_t texel_within_tile;
|
||||
uint32_t coarse_x;
|
||||
uint32_t coarse_y;
|
||||
};
|
||||
|
||||
inline ZOrderOffset ZOrderTileOffset(uint32_t x, uint32_t y) {
|
||||
// Images are split into 8x8 tiles. Each tile is composed of four 4x4 subtiles each
|
||||
// of which is composed of four 2x2 subtiles each of which is composed of four texels.
|
||||
// Each structure is embedded into the next-bigger one in a diagonal pattern, e.g.
|
||||
// texels are laid out in a 2x2 subtile like this:
|
||||
// 2 3
|
||||
// 0 1
|
||||
//
|
||||
// The full 8x8 tile has the texels arranged like this:
|
||||
//
|
||||
// 42 43 46 47 58 59 62 63
|
||||
// 40 41 44 45 56 57 60 61
|
||||
// 34 35 38 39 50 51 54 55
|
||||
// 32 33 36 37 48 49 52 53
|
||||
// 10 11 14 15 26 27 30 31
|
||||
// 08 09 12 13 24 25 28 29
|
||||
// 02 03 06 07 18 19 22 23
|
||||
// 00 01 04 05 16 17 20 21
|
||||
|
||||
// TODO: More flexible encoding algorithms exist for this!
|
||||
uint32_t texel_index_within_tile = 0;
|
||||
for (int block_size_index = 0; block_size_index < 3; ++block_size_index) {
|
||||
int sub_tile_width = 1 << block_size_index;
|
||||
int sub_tile_height = 1 << block_size_index;
|
||||
|
||||
int sub_tile_index = (x & sub_tile_width) << block_size_index;
|
||||
sub_tile_index += 2 * ((y & sub_tile_height) << block_size_index);
|
||||
texel_index_within_tile += sub_tile_index;
|
||||
}
|
||||
|
||||
uint32_t coarse_x = (x / ZOrderOffset::block_width) * ZOrderOffset::block_width;
|
||||
uint32_t coarse_y = (y / ZOrderOffset::block_height) * ZOrderOffset::block_height;
|
||||
|
||||
return { texel_index_within_tile, coarse_x, coarse_y };
|
||||
}
|
||||
|
||||
} // namespace Pica
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue