Merge branch 'master' into vfp-timing

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

43
.github/mac-bundle.sh vendored Executable file
View file

@ -0,0 +1,43 @@
# Taken from pcsx-redux create-app-bundle.sh
# For Plist buddy
PATH="$PATH:/usr/libexec"
# Construct the app iconset.
mkdir alber.iconset
convert docs/img/alber-icon.ico -alpha on -background none -units PixelsPerInch -density 72 -resize 16x16 alber.iconset/icon_16x16.png
convert docs/img/alber-icon.ico -alpha on -background none -units PixelsPerInch -density 144 -resize 32x32 alber.iconset/icon_16x16@2x.png
convert docs/img/alber-icon.ico -alpha on -background none -units PixelsPerInch -density 72 -resize 32x32 alber.iconset/icon_32x32.png
convert docs/img/alber-icon.ico -alpha on -background none -units PixelsPerInch -density 144 -resize 64x64 alber.iconset/icon_32x32@2x.png
convert docs/img/alber-icon.ico -alpha on -background none -units PixelsPerInch -density 72 -resize 128x128 alber.iconset/icon_128x128.png
convert docs/img/alber-icon.ico -alpha on -background none -units PixelsPerInch -density 144 -resize 256x256 alber.iconset/icon_128x128@2x.png
convert docs/img/alber-icon.ico -alpha on -background none -units PixelsPerInch -density 72 -resize 256x256 alber.iconset/icon_256x256.png
convert docs/img/alber-icon.ico -alpha on -background none -units PixelsPerInch -density 144 -resize 512x512 alber.iconset/icon_256x256@2x.png
convert docs/img/alber-icon.ico -alpha on -background none -units PixelsPerInch -density 72 -resize 512x512 alber.iconset/icon_512x512.png
convert docs/img/alber-icon.ico -alpha on -background none -units PixelsPerInch -density 144 -resize 1024x1024 alber.iconset/icon_512x512@2x.png
iconutil --convert icns alber.iconset
# Set up the .app directory
mkdir -p Alber.app/Contents/MacOS/Libraries
mkdir Alber.app/Contents/Resources
# Copy binary into App
cp ./build/Alber Alber.app/Contents/MacOS/Alber
chmod a+x Alber.app/Contents/Macos/Alber
# Copy icons into App
cp alber.icns Alber.app/Contents/Resources/AppIcon.icns
# Fix up Plist stuff
PlistBuddy Alber.app/Contents/Info.plist -c "add CFBundleDisplayName string Alber"
PlistBuddy Alber.app/Contents/Info.plist -c "add CFBundleIconName string AppIcon"
PlistBuddy Alber.app/Contents/Info.plist -c "add CFBundleIconFile string AppIcon"
PlistBuddy Alber.app/Contents/Info.plist -c "add NSHighResolutionCapable bool true"
PlistBuddy Alber.app/Contents/version.plist -c "add ProjectName string Alber"
# Bundle dylibs
dylibbundler -od -b -x Alber.app/Contents/MacOS/Alber -d Alber.app/Contents/Frameworks/ -p @rpath -s /Users/runner/work/Panda3DS/Panda3DS/VULKAN_SDK/lib
# relative rpath
install_name_tool -add_rpath @loader_path/../Frameworks Alber.app/Contents/MacOS/Alber

40
.github/workflows/HTTP_Build.yml vendored Normal file
View file

@ -0,0 +1,40 @@
name: HTTP Server Build
on:
push:
branches:
- master
pull_request:
env:
# Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.)
BUILD_TYPE: Release
jobs:
build:
# The CMake configure and build commands are platform agnostic and should work equally
# well on Windows or Mac. You can convert this to a matrix build if you need
# cross-platform coverage.
# See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Fetch submodules
run: git submodule update --init --recursive
- name: Setup Vulkan SDK
uses: humbletim/setup-vulkan-sdk@v1.2.0
with:
vulkan-query-version: latest
vulkan-use-cache: true
vulkan-components: Vulkan-Headers, Vulkan-Loader, Glslang
- name: Configure CMake
# Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make.
# See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type
run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DENABLE_USER_BUILD=ON -DENABLE_HTTP_SERVER=ON
- name: Build
# Build your program with the given configuration
run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}}

View file

@ -23,6 +23,16 @@ jobs:
- name: Fetch submodules
run: git submodule update --init --recursive
- name: Install misc packages
run: sudo apt-get update && sudo apt install libx11-dev libgl1-mesa-glx mesa-common-dev
- name: Setup Vulkan SDK
uses: humbletim/setup-vulkan-sdk@v1.2.0
with:
vulkan-query-version: latest
vulkan-use-cache: true
vulkan-components: Vulkan-Headers, Vulkan-Loader, Glslang
- name: Configure CMake
# Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make.
# See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type

View file

@ -23,6 +23,13 @@ jobs:
- name: Fetch submodules
run: git submodule update --init --recursive
- name: Setup Vulkan SDK
uses: humbletim/setup-vulkan-sdk@v1.2.0
with:
vulkan-query-version: latest
vulkan-use-cache: true
vulkan-components: Vulkan-Headers, Vulkan-Loader, Glslang
- name: Configure CMake
# Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make.
# See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type
@ -32,8 +39,20 @@ jobs:
# Build your program with the given configuration
run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}}
- name: Upload executable
- name: Install bundle dependencies
run: brew install dylibbundler imagemagick
- name: Run bundle script
run: ./.github/mac-bundle.sh
- name: Sign the App
run: codesign --force -s - -vvvv Alber.app
- name: Zip it up
run: zip -r Alber Alber.app
- name: Upload MacOS App
uses: actions/upload-artifact@v2
with:
name: MacOS executable
path: './build/Alber'
name: MacOS Alber App Bundle
path: 'Alber.zip'

View file

@ -23,6 +23,13 @@ jobs:
- name: Fetch submodules
run: git submodule update --init --recursive
- name: Setup Vulkan SDK
uses: humbletim/setup-vulkan-sdk@v1.2.0
with:
vulkan-query-version: latest
vulkan-use-cache: true
vulkan-components: Vulkan-Headers, Vulkan-Loader, Glslang
- name: Configure CMake
# Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make.
# See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type

6
.gitignore vendored
View file

@ -55,6 +55,12 @@ fb.bat
*.idb
# 3DS files
*.3ds
*.3dsx
*.app
*.cci
*.cxi
*.elf
*.smdh
config.toml

9
.gitmodules vendored
View file

@ -25,3 +25,12 @@
[submodule "stb"]
path = third_party/stb
url = https://github.com/nothings/stb
[submodule "third_party/cmrc"]
path = third_party/cmrc
url = https://github.com/vector-of-bool/cmrc
[submodule "third_party/glm"]
path = third_party/glm
url = https://github.com/g-truc/glm
[submodule "third_party/discord-rpc"]
path = third_party/discord-rpc
url = https://github.com/Panda3DS-emu/discord-rpc

View file

@ -1,4 +1,11 @@
cmake_minimum_required(VERSION 3.10)
# We need to be able to use enable_language(OBJC) on Mac, so we need CMake 3.16 vs the 3.10 we use otherwise. Blame Apple.
if (APPLE)
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0" CACHE STRING "Minimum OS X deployment version")
cmake_minimum_required(VERSION 3.16)
else()
cmake_minimum_required(VERSION 3.10)
endif()
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)
@ -7,12 +14,16 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION VERSION
endif()
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
set(CMAKE_BUILD_TYPE Release)
endif()
project(Alber)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR})
if(APPLE)
enable_language(OBJC)
endif()
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-format-nonliteral -Wno-format-security")
endif()
@ -20,9 +31,11 @@ endif()
option(DISABLE_PANIC_DEV "Make a build with fewer and less intrusive asserts" OFF)
option(GPU_DEBUG_INFO "Enable additional GPU debugging info" OFF)
option(ENABLE_OPENGL "Enable OpenGL rendering backend" ON)
option(ENABLE_VULKAN "Enable Vulkan rendering backend" ON)
option(ENABLE_LTO "Enable link-time optimization" OFF)
option(ENABLE_USER_BUILD "Make a user-facing build. These builds have various assertions disabled, LTO, and more" OFF)
option(ENABLE_HTTP_SERVER "Enable HTTP server. Used for Discord bot support" OFF)
option(ENABLE_DISCORD_RPC "Compile with Discord RPC support (disabled by default)" ON)
include_directories(${PROJECT_SOURCE_DIR}/include/)
include_directories(${PROJECT_SOURCE_DIR}/include/kernel)
@ -37,11 +50,17 @@ include_directories(third_party/result/include)
include_directories(third_party/xxhash/include)
include_directories(third_party/httplib)
include_directories(third_party/stb)
include_directories(third_party/opengl)
add_compile_definitions(NOMINMAX) # Make windows.h not define min/max macros because third-party deps don't like it
add_compile_definitions(WIN32_LEAN_AND_MEAN) # Make windows.h not include literally everything
add_compile_definitions(SDL_MAIN_HANDLED)
if(ENABLE_DISCORD_RPC AND NOT ANDROID)
add_subdirectory(third_party/discord-rpc)
include_directories(third_party/discord-rpc/include)
endif()
set(SDL_STATIC ON CACHE BOOL "" FORCE)
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
set(SDL_TEST OFF CACHE BOOL "" FORCE)
@ -50,6 +69,9 @@ add_subdirectory(third_party/SDL2)
add_subdirectory(third_party/toml11)
include_directories(${SDL2_INCLUDE_DIR})
include_directories(third_party/toml11)
include_directories(third_party/glm)
add_subdirectory(third_party/cmrc)
set(BOOST_ROOT "${CMAKE_SOURCE_DIR}/third_party/boost")
set(Boost_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/third_party/boost")
@ -61,6 +83,7 @@ target_include_directories(boost SYSTEM INTERFACE ${Boost_INCLUDE_DIR})
set(CRYPTOPP_BUILD_TESTING OFF)
add_subdirectory(third_party/cryptopp)
add_subdirectory(third_party/glad)
# Check for x64
if (CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" OR CMAKE_SYSTEM_PROCESSOR STREQUAL "x86-64" OR CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
@ -93,7 +116,9 @@ endif()
set(SOURCE_FILES src/main.cpp src/emulator.cpp src/io_file.cpp src/config.cpp
src/core/CPU/cpu_dynarmic.cpp src/core/CPU/dynarmic_cycles.cpp
src/core/memory.cpp src/renderer.cpp src/httpserver.cpp src/stb_image_write.c
src/core/memory.cpp src/renderer.cpp src/core/renderer_null/renderer_null.cpp
src/http_server.cpp src/stb_image_write.c src/core/cheats.cpp src/core/action_replay.cpp
src/discord_rpc.cpp
)
set(CRYPTO_SOURCE_FILES src/core/crypto/aes_engine.cpp)
set(KERNEL_SOURCE_FILES src/core/kernel/kernel.cpp src/core/kernel/resource_limits.cpp
@ -101,17 +126,18 @@ set(KERNEL_SOURCE_FILES src/core/kernel/kernel.cpp src/core/kernel/resource_limi
src/core/kernel/events.cpp src/core/kernel/threads.cpp
src/core/kernel/address_arbiter.cpp src/core/kernel/error.cpp
src/core/kernel/file_operations.cpp src/core/kernel/directory_operations.cpp
src/core/kernel/idle_thread.cpp
src/core/kernel/idle_thread.cpp src/core/kernel/timers.cpp
)
set(SERVICE_SOURCE_FILES src/core/services/service_manager.cpp src/core/services/apt.cpp src/core/services/hid.cpp
src/core/services/fs.cpp src/core/services/gsp_gpu.cpp src/core/services/gsp_lcd.cpp
src/core/services/ndm.cpp src/core/services/dsp.cpp src/core/services/cfg.cpp
src/core/services/ptm.cpp src/core/services/mic.cpp src/core/services/cecd.cpp
src/core/services/ac.cpp src/core/services/am.cpp src/core/services/boss.cpp
src/core/services/frd.cpp src/core/services/nim.cpp src/core/services/shared_font.cpp
src/core/services/frd.cpp src/core/services/nim.cpp src/core/services/mcu/mcu_hwc.cpp
src/core/services/y2r.cpp src/core/services/cam.cpp src/core/services/ldr_ro.cpp
src/core/services/act.cpp src/core/services/nfc.cpp src/core/services/dlp_srvr.cpp
src/core/services/ir_user.cpp
src/core/services/ir_user.cpp src/core/services/http.cpp src/core/services/soc.cpp
src/core/services/ssl.cpp src/core/services/news_u.cpp
)
set(PICA_SOURCE_FILES src/core/PICA/gpu.cpp src/core/PICA/regs.cpp src/core/PICA/shader_unit.cpp
src/core/PICA/shader_interpreter.cpp src/core/PICA/dynapica/shader_rec.cpp
@ -120,15 +146,18 @@ set(PICA_SOURCE_FILES src/core/PICA/gpu.cpp src/core/PICA/regs.cpp src/core/PICA
set(LOADER_SOURCE_FILES src/core/loader/elf.cpp src/core/loader/ncsd.cpp src/core/loader/ncch.cpp src/core/loader/lz77.cpp)
set(FS_SOURCE_FILES src/core/fs/archive_self_ncch.cpp src/core/fs/archive_save_data.cpp src/core/fs/archive_sdmc.cpp
src/core/fs/archive_ext_save_data.cpp src/core/fs/archive_ncch.cpp
src/core/fs/archive_ext_save_data.cpp src/core/fs/archive_ncch.cpp src/core/fs/romfs.cpp
src/core/fs/ivfc.cpp
)
set(RENDERER_SW_SOURCE_FILES src/core/renderer_sw/renderer_sw.cpp)
set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp
include/cpu.hpp include/cpu_dynarmic.hpp include/memory.hpp include/renderer.hpp include/kernel/kernel.hpp
include/dynarmic_cp15.hpp include/kernel/resource_limits.hpp include/kernel/kernel_types.hpp
include/kernel/config_mem.hpp include/services/service_manager.hpp include/services/apt.hpp
include/kernel/handles.hpp include/services/hid.hpp include/services/fs.hpp
include/services/gsp_gpu.hpp include/services/gsp_lcd.hpp include/arm_defs.hpp
include/services/gsp_gpu.hpp include/services/gsp_lcd.hpp include/arm_defs.hpp include/renderer_null/renderer_null.hpp
include/PICA/gpu.hpp include/PICA/regs.hpp include/services/ndm.hpp
include/PICA/shader.hpp include/PICA/shader_unit.hpp include/PICA/float_types.hpp
include/logger.hpp include/loader/ncch.hpp include/loader/ncsd.hpp include/io_file.hpp
@ -137,8 +166,8 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp
include/fs/archive_save_data.hpp include/fs/archive_sdmc.hpp include/services/ptm.hpp
include/services/mic.hpp include/services/cecd.hpp include/services/ac.hpp
include/services/am.hpp include/services/boss.hpp include/services/frd.hpp include/services/nim.hpp
include/fs/archive_ext_save_data.hpp include/services/shared_font.hpp include/fs/archive_ncch.hpp
include/colour.hpp include/services/y2r.hpp include/services/cam.hpp
include/fs/archive_ext_save_data.hpp include/fs/archive_ncch.hpp include/services/mcu/mcu_hwc.hpp
include/colour.hpp include/services/y2r.hpp include/services/cam.hpp include/services/ssl.hpp
include/services/ldr_ro.hpp include/ipc.hpp include/services/act.hpp include/services/nfc.hpp
include/system_models.hpp include/services/dlp_srvr.hpp include/PICA/dynapica/pica_recs.hpp
include/PICA/dynapica/x64_regs.hpp include/PICA/dynapica/vertex_loader_rec.hpp include/PICA/dynapica/shader_rec.hpp
@ -146,7 +175,17 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp
include/result/result_common.hpp include/result/result_fs.hpp include/result/result_fnd.hpp
include/result/result_gsp.hpp include/result/result_kernel.hpp include/result/result_os.hpp
include/crypto/aes_engine.hpp include/metaprogramming.hpp include/PICA/pica_vertex.hpp
include/config.hpp include/services/ir_user.hpp include/httpserver.hpp
include/config.hpp include/services/ir_user.hpp include/http_server.hpp include/cheats.hpp
include/action_replay.hpp include/renderer_sw/renderer_sw.hpp include/compiler_builtins.hpp
include/fs/romfs.hpp include/fs/ivfc.hpp include/discord_rpc.hpp include/services/http.hpp include/result/result_cfg.hpp
include/math_util.hpp include/services/soc.hpp include/services/news_u.hpp
)
cmrc_add_resource_library(
resources_console_fonts
NAMESPACE ConsoleFonts
WHENCE "src/core/services/fonts/"
"src/core/services/fonts/CitraSharedFontUSRelocated.bin"
)
set(THIRD_PARTY_SOURCE_FILES third_party/imgui/imgui.cpp
@ -165,48 +204,100 @@ source_group("Source Files\\Core\\Kernel" FILES ${KERNEL_SOURCE_FILES})
source_group("Source Files\\Core\\Loader" FILES ${LOADER_SOURCE_FILES})
source_group("Source Files\\Core\\Services" FILES ${SERVICE_SOURCE_FILES})
source_group("Source Files\\Core\\PICA" FILES ${PICA_SOURCE_FILES})
source_group("Source Files\\Core\\Software Renderer" FILES ${RENDERER_SW_SOURCE_FILES})
source_group("Source Files\\Third Party" FILES ${THIRD_PARTY_SOURCE_FILES})
set(RENDERER_GL_SOURCE_FILES "") # Empty by default unless we are compiling with the GL renderer
set(RENDERER_VK_SOURCE_FILES "") # Empty by default unless we are compiling with the VK renderer
if(ENABLE_OPENGL)
add_subdirectory(third_party/glad)
set(RENDERER_GL_INCLUDE_FILES include/renderer_gl/opengl.hpp
# This may look weird but opengl.hpp is our header even if it's in the third_party folder
set(RENDERER_GL_INCLUDE_FILES third_party/opengl/opengl.hpp
include/renderer_gl/renderer_gl.hpp include/renderer_gl/textures.hpp
include/renderer_gl/surfaces.hpp include/renderer_gl/surface_cache.hpp
include/renderer_gl/gl_state.hpp
)
set(RENDERER_GL_SOURCE_FILES src/core/renderer_gl/renderer_gl.cpp
src/core/renderer_gl/textures.cpp src/core/renderer_gl/etc1.cpp
src/core/renderer_gl/gl_state.cpp
src/core/renderer_gl/textures.cpp src/core/renderer_gl/etc1.cpp
src/core/renderer_gl/gl_state.cpp src/host_shaders/opengl_display.frag
src/host_shaders/opengl_display.vert src/host_shaders/opengl_vertex_shader.vert
src/host_shaders/opengl_fragment_shader.frag
)
set(HEADER_FILES ${HEADER_FILES} ${RENDERER_GL_INCLUDE_FILES})
source_group("Source Files\\Core\\OpenGL Renderer" FILES ${RENDERER_GL_SOURCE_FILES})
cmrc_add_resource_library(
resources_renderer_gl
NAMESPACE RendererGL
WHENCE "src/host_shaders/"
"src/host_shaders/opengl_display.frag"
"src/host_shaders/opengl_display.vert"
"src/host_shaders/opengl_vertex_shader.vert"
"src/host_shaders/opengl_fragment_shader.frag"
)
endif()
if(ENABLE_VULKAN)
find_package(
Vulkan 1.3.206 REQUIRED
COMPONENTS glslangValidator
)
set(RENDERER_VK_INCLUDE_FILES include/renderer_vk/renderer_vk.hpp
include/renderer_vk/vulkan_api.hpp include/renderer_vk/vk_debug.hpp
)
set(RENDERER_VK_SOURCE_FILES src/core/renderer_vk/renderer_vk.cpp
src/core/renderer_vk/vulkan_api.cpp src/core/renderer_vk/vk_debug.cpp
)
set(HEADER_FILES ${HEADER_FILES} ${RENDERER_VK_INCLUDE_FILES})
source_group("Source Files\\Core\\Vulkan Renderer" FILES ${RENDERER_VK_SOURCE_FILES})
cmrc_add_resource_library(
resources_renderer_vk
NAMESPACE RendererVK
WHENCE "src/host_shaders/"
)
endif()
source_group("Header Files\\Core" FILES ${HEADER_FILES})
set(ALL_SOURCES ${SOURCE_FILES} ${FS_SOURCE_FILES} ${CRYPTO_SOURCE_FILES} ${KERNEL_SOURCE_FILES} ${LOADER_SOURCE_FILES} ${SERVICE_SOURCE_FILES}
${PICA_SOURCE_FILES} ${THIRD_PARTY_SOURCE_FILES} ${HEADER_FILES})
${RENDERER_SW_SOURCE_FILES} ${PICA_SOURCE_FILES} ${THIRD_PARTY_SOURCE_FILES} ${HEADER_FILES})
if(ENABLE_OPENGL)
# Add the OpenGL source files to ALL_SOURCES
set(ALL_SOURCES ${ALL_SOURCES} ${RENDERER_GL_SOURCE_FILES})
endif()
if(ENABLE_VULKAN)
# Add the Vulkan source files to ALL_SOURCES
set(ALL_SOURCES ${ALL_SOURCES} ${RENDERER_VK_SOURCE_FILES})
endif()
add_executable(Alber ${ALL_SOURCES})
if(ENABLE_LTO OR ENABLE_USER_BUILD)
set_target_properties(Alber PROPERTIES INTERPROCEDURAL_OPTIMIZATION TRUE)
set_target_properties(Alber PROPERTIES INTERPROCEDURAL_OPTIMIZATION TRUE)
endif()
target_link_libraries(Alber PRIVATE dynarmic SDL2-static cryptopp)
target_link_libraries(Alber PRIVATE dynarmic SDL2-static cryptopp glad resources_console_fonts)
if(ENABLE_DISCORD_RPC AND NOT ANDROID)
target_compile_definitions(Alber PUBLIC "PANDA3DS_ENABLE_DISCORD_RPC=1")
target_link_libraries(Alber PRIVATE discord-rpc)
endif()
if(ENABLE_OPENGL)
target_compile_definitions(Alber PUBLIC "PANDA3DS_ENABLE_OPENGL=1")
target_link_libraries(Alber PRIVATE glad)
target_link_libraries(Alber PRIVATE resources_renderer_gl)
endif()
if(ENABLE_VULKAN)
target_compile_definitions(Alber PUBLIC "PANDA3DS_ENABLE_VULKAN=1")
target_link_libraries(Alber PRIVATE Vulkan::Vulkan resources_renderer_vk)
endif()
if(GPU_DEBUG_INFO)

BIN
docs/img/KirbyRobobot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 360 KiB

After

Width:  |  Height:  |  Size: 389 KiB

BIN
docs/img/alber-icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 MiB

View file

@ -91,6 +91,7 @@ class ShaderEmitter : public Xbyak::CodeGenerator {
void recCMP(const PICAShader& shader, u32 instruction);
void recDP3(const PICAShader& shader, u32 instruction);
void recDP4(const PICAShader& shader, u32 instruction);
void recDPH(const PICAShader& shader, u32 instruction);
void recEMIT(const PICAShader& shader, u32 instruction);
void recEND(const PICAShader& shader, u32 instruction);
void recEX2(const PICAShader& shader, u32 instruction);
@ -111,7 +112,6 @@ class ShaderEmitter : public Xbyak::CodeGenerator {
void recRSQ(const PICAShader& shader, u32 instruction);
void recSETEMIT(const PICAShader& shader, u32 instruction);
void recSGE(const PICAShader& shader, u32 instruction);
void recSGEI(const PICAShader& shader, u32 instruction);
void recSLT(const PICAShader& shader, u32 instruction);
MAKE_LOG_FUNCTION(log, shaderJITLogger)

View file

@ -14,8 +14,11 @@
class GPU {
static constexpr u32 regNum = 0x300;
static constexpr u32 extRegNum = 0x1000;
using vec4f = std::array<Floats::f24, 4>;
using Registers = std::array<u32, regNum>;
using Registers = std::array<u32, regNum>; // Internal registers (named registers in short since they're the main ones)
using ExternalRegisters = std::array<u32, extRegNum>;
Memory& mem;
EmulatorConfig& config;
@ -83,7 +86,7 @@ class GPU {
bool lightingLUTDirty = false;
GPU(Memory& mem, EmulatorConfig& config);
void initGraphicsContext() { renderer->initGraphicsContext(); }
void initGraphicsContext(SDL_Window* window) { renderer->initGraphicsContext(window); }
void display() { renderer->display(); }
void screenshot(const std::string& name) { renderer->screenshot(name); }
@ -91,21 +94,23 @@ class GPU {
void reset();
Registers& getRegisters() { return regs; }
ExternalRegisters& getExtRegisters() { return externalRegs; }
void startCommandList(u32 addr, u32 size);
// Used by the GSP GPU service for readHwRegs/writeHwRegs/writeHwRegsMasked
u32 readReg(u32 address);
void writeReg(u32 address, u32 value);
u32 readExternalReg(u32 index);
void writeExternalReg(u32 index, u32 value);
// Used when processing GPU command lists
u32 readInternalReg(u32 index);
void writeInternalReg(u32 index, u32 value, u32 mask);
// TODO: Emulate the transfer engine & its registers
// Then this can be emulated by just writing the appropriate values there
void clearBuffer(u32 startAddress, u32 endAddress, u32 value, u32 control) {
renderer->clearBuffer(startAddress, endAddress, value, control);
}
void clearBuffer(u32 startAddress, u32 endAddress, u32 value, u32 control) { renderer->clearBuffer(startAddress, endAddress, value, control); }
// TODO: Emulate the transfer engine & its registers
// Then this can be emulated by just writing the appropriate values there
@ -113,6 +118,10 @@ class GPU {
renderer->displayTransfer(inputAddr, outputAddr, inputSize, outputSize, flags);
}
void textureCopy(u32 inputAddr, u32 outputAddr, u32 totalBytes, u32 inputSize, u32 outputSize, u32 flags) {
renderer->textureCopy(inputAddr, outputAddr, totalBytes, inputSize, outputSize, flags);
}
// Read a value of type T from physical address paddr
// This is necessary because vertex attribute fetching uses physical addresses
template <typename T>
@ -129,17 +138,23 @@ class GPU {
// Get a pointer of type T* to the data starting from physical address paddr
template <typename T>
T* getPointerPhys(u32 paddr) {
if (paddr >= PhysicalAddrs::FCRAM && paddr <= PhysicalAddrs::FCRAMEnd) {
T* getPointerPhys(u32 paddr, u32 size = 0) {
if (paddr >= PhysicalAddrs::FCRAM && paddr + size <= PhysicalAddrs::FCRAMEnd) {
u8* fcram = mem.getFCRAM();
u32 index = paddr - PhysicalAddrs::FCRAM;
return (T*)&fcram[index];
} else if (paddr >= PhysicalAddrs::VRAM && paddr <= PhysicalAddrs::VRAMEnd) {
} else if (paddr >= PhysicalAddrs::VRAM && paddr + size <= PhysicalAddrs::VRAMEnd) {
u32 index = paddr - PhysicalAddrs::VRAM;
return (T*)&vram[index];
} else [[unlikely]] {
Helpers::panic("[GPU] Tried to access unknown physical address: %08X", paddr);
}
}
private:
// GPU external registers
// We have them in the end of the struct for cache locality reasons. Tl;dr we want the more commonly used things to be packed in the start
// Of the struct, instead of externalRegs being in the middle
ExternalRegisters externalRegs;
};

View file

@ -22,6 +22,7 @@ namespace PICA {
ShaderOutputCount = 0x4F,
ShaderOutmap0 = 0x50,
ViewportXY = 0x68,
DepthmapEnable = 0x6D,
// Texture registers
@ -53,9 +54,13 @@ namespace PICA {
// Framebuffer registers
ColourOperation = 0x100,
BlendFunc = 0x101,
LogicOp = 0x102,
BlendColour = 0x103,
AlphaTestConfig = 0x104,
StencilTest = 0x105,
StencilOp = 0x106,
DepthAndColorMask = 0x107,
DepthBufferWrite = 0x115,
DepthBufferFormat = 0x116,
ColourBufferFormat = 0x117,
DepthBufferLoc = 0x11C,
@ -174,6 +179,53 @@ namespace PICA {
};
}
namespace ExternalRegs {
enum : u32 {
MemFill1BufferStartPaddr = 0x3,
MemFill1BufferEndPAddr = 0x4,
MemFill1Value = 0x5,
MemFill1Control = 0x6,
MemFill2BufferStartPaddr = 0x7,
MemFill2BufferEndPAddr = 0x8,
MemFill2Value = 0x9,
MemFill2Control = 0xA,
VramBankControl = 0xB,
GPUBusy = 0xC,
BacklightControl = 0xBC,
Framebuffer0Size = 0x118,
Framebuffer0AFirstAddr = 0x119,
Framebuffer0ASecondAddr = 0x11A,
Framebuffer0Config = 0x11B,
Framebuffer0Select = 0x11D,
Framebuffer0Stride = 0x123,
Framebuffer0BFirstAddr = 0x124,
Framebuffer0BSecondAddr = 0x125,
Framebuffer1Size = 0x156,
Framebuffer1AFirstAddr = 0x159,
Framebuffer1ASecondAddr = 0x15A,
Framebuffer1Config = 0x15B,
Framebuffer1Select = 0x15D,
Framebuffer1Stride = 0x163,
Framebuffer1BFirstAddr = 0x164,
Framebuffer1BSecondAddr = 0x165,
TransferInputPAddr = 0x2FF,
TransferOutputPAddr = 0x300,
DisplayTransferOutputDim = 0x301,
DisplayTransferInputDim = 0x302,
TransferFlags = 0x303,
TransferTrigger = 0x305,
TextureCopyTotalBytes = 0x307,
TextureCopyInputLineGap = 0x308,
TextureCopyOutputLineGap = 0x309,
};
}
enum class Scaling : u32 {
None = 0,
X = 1,
XY = 2,
};
namespace Lights {
enum : u32 {
LUT_D0 = 0,

View file

@ -23,6 +23,7 @@ namespace ShaderOpcodes {
LG2 = 0x06,
LIT = 0x07,
MUL = 0x08,
SGE = 0x09,
SLT = 0x0A,
FLR = 0x0B,
MAX = 0x0C,
@ -158,6 +159,7 @@ class PICAShader {
void mul(u32 instruction);
void rcp(u32 instruction);
void rsq(u32 instruction);
void sge(u32 instruction);
void sgei(u32 instruction);
void slt(u32 instruction);
void slti(u32 instruction);

52
include/action_replay.hpp Normal file
View file

@ -0,0 +1,52 @@
#pragma once
#include <array>
#include <bitset>
#include <vector>
#include "helpers.hpp"
#include "memory.hpp"
#include "services/hid.hpp"
class ActionReplay {
using Cheat = std::vector<u32>; // A cheat is really just a bunch of 64-bit opcodes neatly encoded into 32-bit chunks
static constexpr size_t ifStackSize = 32; // TODO: How big is this, really?
u32 offset1, offset2; // Memory offset registers. Non-persistent.
u32 data1, data2; // Data offset registers. Non-persistent.
u32 storage1, storage2; // Storage registers. Persistent.
// When an instruction does not specify which offset or data register to use, we use the "active" one
// Which is by default #1 and may be changed by certain AR operations
u32 *activeOffset, *activeData, *activeStorage;
u32 ifStackIndex; // Our index in the if stack. Shows how many entries we have at the moment.
u32 loopStackIndex; // Same but for loops
std::bitset<32> ifStack;
// Program counter
u32 pc = 0;
Memory& mem;
HIDService& hid;
// Has the cheat ended?
bool running = false;
// Run 1 AR instruction
void runInstruction(const Cheat& cheat, u32 instruction);
// Action Replay has a billion D-type opcodes so this handles all of them
void executeDType(const Cheat& cheat, u32 instruction);
u8 read8(u32 addr);
u16 read16(u32 addr);
u32 read32(u32 addr);
void write8(u32 addr, u8 value);
void write16(u32 addr, u16 value);
void write32(u32 addr, u32 value);
void pushConditionBlock(bool condition);
public:
ActionReplay(Memory& mem, HIDService& hid);
void runCheat(const Cheat& cheat);
void reset();
};

36
include/cheats.hpp Normal file
View file

@ -0,0 +1,36 @@
#pragma once
#include <array>
#include <vector>
#include "action_replay.hpp"
#include "helpers.hpp"
#include "services/hid.hpp"
// Forward-declare this since it's just passed and we don't want to include memory.hpp and increase compile time
class Memory;
class Cheats {
public:
enum class CheatType {
ActionReplay, // CTRPF cheats
// TODO: Other cheat devices and standards?
};
struct Cheat {
CheatType type;
std::vector<u32> instructions;
};
Cheats(Memory& mem, HIDService& hid);
void addCheat(const Cheat& cheat);
void reset();
void run();
void clear();
bool haveCheats() const { return cheatsLoaded; }
private:
ActionReplay ar; // An ActionReplay cheat machine for executing CTRPF codes
std::vector<Cheat> cheats;
bool cheatsLoaded = false;
};

View file

@ -0,0 +1,7 @@
#pragma once
#ifdef _MSC_VER
#define ALWAYS_INLINE __forceinline
#else
#define ALWAYS_INLINE __attribute__((always_inline))
#endif

View file

@ -1,10 +1,22 @@
#pragma once
#include <filesystem>
#include "renderer.hpp"
// Remember to initialize every field here to its default value otherwise bad things will happen
struct EmulatorConfig {
bool shaderJitEnabled = false;
bool shaderJitEnabled = true;
bool discordRpcEnabled = false;
RendererType rendererType = RendererType::OpenGL;
bool sdCardInserted = true;
bool sdWriteProtected = false;
bool chargerPlugged = true;
// Default to 3% battery to make users suffer
int batteryPercentage = 3;
EmulatorConfig(const std::filesystem::path& path);
void load(const std::filesystem::path& path);
void save(const std::filesystem::path& path);
};

View file

@ -108,7 +108,7 @@ public:
return getCyclesForInstruction(isThumb, instruction);
}
MyEnvironment(Memory& mem, Kernel& kernel, CPU& cpu) : mem(mem), kernel(kernel) {}
MyEnvironment(Memory& mem, Kernel& kernel) : mem(mem), kernel(kernel) {}
};
class CPU {

View file

@ -105,6 +105,7 @@ namespace Crypto {
AESEngine() {}
void loadKeys(const std::filesystem::path& path);
bool haveKeys() { return keysLoaded; }
bool haveGenerator() { return m_generator.has_value(); }
constexpr bool hasKeyX(std::size_t slotId) {
if (slotId >= AesKeySlotCount) {

23
include/discord_rpc.hpp Normal file
View file

@ -0,0 +1,23 @@
#pragma once
#ifdef PANDA3DS_ENABLE_DISCORD_RPC
#include <discord_rpc.h>
#include <cstdint>
#include <string>
namespace Discord {
enum class RPCStatus { Idling, Playing };
class RPC {
std::uint64_t startTimestamp;
bool enabled = false;
public:
void init();
void update(RPCStatus status, const std::string& title);
void stop();
};
} // namespace Discord
#endif

View file

@ -7,14 +7,16 @@
#include <optional>
#include "PICA/gpu.hpp"
#include "cheats.hpp"
#include "config.hpp"
#include "cpu.hpp"
#include "crypto/aes_engine.hpp"
#include "discord_rpc.hpp"
#include "io_file.hpp"
#include "memory.hpp"
#ifdef PANDA3DS_ENABLE_HTTP_SERVER
#include "httpserver.hpp"
#include "http_server.hpp"
#endif
enum class ROMType {
@ -25,13 +27,14 @@ enum class ROMType {
};
class Emulator {
EmulatorConfig config;
CPU cpu;
GPU gpu;
Memory memory;
Kernel kernel;
Crypto::AESEngine aesEngine;
Cheats cheats;
EmulatorConfig config;
SDL_Window* window;
#ifdef PANDA3DS_ENABLE_OPENGL
@ -54,12 +57,19 @@ class Emulator {
static constexpr u32 width = 400;
static constexpr u32 height = 240 * 2; // * 2 because 2 screens
ROMType romType = ROMType::None;
bool running = true;
bool running = false; // Is the emulator running a game?
bool programRunning = false; // Is the emulator program itself running?
#ifdef PANDA3DS_ENABLE_HTTP_SERVER
HttpServer httpServer;
friend struct HttpServer;
#endif
#ifdef PANDA3DS_ENABLE_DISCORD_RPC
Discord::RPC discordRpc;
#endif
void updateDiscord();
// Keep the handle for the ROM here to reload when necessary and to prevent deleting it
// This is currently only used for ELFs, NCSDs use the IOFile API instead
std::ifstream loadedELF;
@ -70,8 +80,8 @@ class Emulator {
public:
// Decides whether to reload or not reload the ROM when resetting. We use enum class over a plain bool for clarity.
// If NoReload is selected, the emulator will not reload its selected ROM. This is useful for things like booting up the emulator, or resetting to
// change ROMs. If Reload is selected, the emulator will reload its selected ROM. This is useful for eg a "reset" button that keeps the current ROM
// and just resets the emu
// change ROMs. If Reload is selected, the emulator will reload its selected ROM. This is useful for eg a "reset" button that keeps the current
// ROM and just resets the emu
enum class ReloadOption { NoReload, Reload };
Emulator();
@ -83,13 +93,13 @@ class Emulator {
void run();
void runFrame();
void resume(); // Resume the emulator
void pause(); // Pause the emulator
void togglePause();
bool loadROM(const std::filesystem::path& path);
bool loadNCSD(const std::filesystem::path& path, ROMType type);
bool loadELF(const std::filesystem::path& path);
bool loadELF(std::ifstream& file);
void initGraphicsContext();
#ifdef PANDA3DS_ENABLE_HTTP_SERVER
void pollHttpServer();
#endif
};

View file

@ -116,15 +116,34 @@ struct ArchiveSession {
ArchiveSession(ArchiveBase* archive, const FSPath& filePath, bool isOpen = true) : archive(archive), path(filePath), isOpen(isOpen) {}
};
struct DirectorySession {
ArchiveBase* archive = nullptr;
// For directories which are mirrored to a specific path on the disk, this contains that path
// Otherwise this is a nullopt
std::optional<std::filesystem::path> pathOnDisk;
bool isOpen;
struct DirectoryEntry {
std::filesystem::path path;
bool isDirectory;
};
DirectorySession(ArchiveBase* archive, std::filesystem::path path, bool isOpen = true) : archive(archive), pathOnDisk(path),
isOpen(isOpen) {}
struct DirectorySession {
ArchiveBase* archive = nullptr;
// For directories which are mirrored to a specific path on the disk, this contains that path
// Otherwise this is a nullopt
std::optional<std::filesystem::path> pathOnDisk;
// The list of directory entries + the index of the entry we're currently inspecting
std::vector<DirectoryEntry> entries;
size_t currentEntry;
bool isOpen;
DirectorySession(ArchiveBase* archive, std::filesystem::path path, bool isOpen = true) : archive(archive), pathOnDisk(path), isOpen(isOpen) {
currentEntry = 0; // Start from entry 0
// Read all directory entries, cache them
for (auto& e : std::filesystem::directory_iterator(path)) {
DirectoryEntry entry;
entry.path = e.path();
entry.isDirectory = std::filesystem::is_directory(e);
entries.push_back(entry);
}
}
};
// Represents a file descriptor obtained from OpenFile. If the optional is nullopt, opening the file failed.

View file

@ -5,7 +5,7 @@ class SaveDataArchive : public ArchiveBase {
public:
SaveDataArchive(Memory& mem) : ArchiveBase(mem) {}
u64 getFreeBytes() override { Helpers::panic("SaveData::GetFreeBytes unimplemented"); return 0; }
u64 getFreeBytes() override { return 32_MB; }
std::string name() override { return "SaveData"; }
HorizonResult createDirectory(const FSPath& path) override;

20
include/fs/ivfc.hpp Normal file
View file

@ -0,0 +1,20 @@
#pragma once
#include <vector>
#include "helpers.hpp"
namespace IVFC {
struct IVFCLevel {
u64 logicalOffset;
u64 size;
u64 blockSize;
};
struct IVFC {
u64 masterHashSize;
std::vector<IVFCLevel> levels;
};
size_t parseIVFC(uintptr_t ivfcStart, IVFC& ivfc);
} // namespace IVFC

22
include/fs/romfs.hpp Normal file
View file

@ -0,0 +1,22 @@
#pragma once
#include <memory>
#include <string>
#include <vector>
#include "helpers.hpp"
namespace RomFS {
struct RomFSNode {
std::u16string name;
// The file/directory offset relative to the start of the RomFS
u64 metadataOffset = 0;
u64 dataOffset = 0;
u64 dataSize = 0;
bool isDirectory = false;
std::vector<std::unique_ptr<RomFSNode>> directories;
std::vector<std::unique_ptr<RomFSNode>> files;
};
std::unique_ptr<RomFSNode> parseRomFSTree(uintptr_t romFS, u64 romFSSize);
} // namespace RomFS

View file

@ -7,6 +7,7 @@
#include <sstream>
#include <string>
#include <vector>
#include <memory>
#include "termcolor.hpp"
@ -30,6 +31,17 @@ using s32 = std::int32_t;
using s64 = std::int64_t;
namespace Helpers {
template <class... Args>
std::string format(const std::string& fmt, Args&&... args) {
const int size = std::snprintf(nullptr, 0, fmt.c_str(), args...) + 1;
if (size <= 0) {
return {};
}
const auto buf = std::make_unique<char[]>(size);
std::snprintf(buf.get(), size, fmt.c_str(), args ...);
return std::string(buf.get(), buf.get() + size - 1);
}
// Unconditional panic, unlike panicDev which does not panic on user builds
template <class... Args>
[[noreturn]] static void panic(const char* fmt, Args&&... args) {
@ -125,7 +137,7 @@ namespace Helpers {
return getBits<offset, bits, ValueT, ValueT>(value);
}
#ifdef HELPERS_APPLE_CLANG
#if defined(HELPERS_APPLE_CLANG) || defined(__ANDROID__)
template <class To, class From>
constexpr To bit_cast(const From& from) noexcept {
return *reinterpret_cast<const To*>(&from);
@ -155,4 +167,3 @@ namespace Helpers {
constexpr size_t operator""_KB(unsigned long long int x) { return 1024ULL * x; }
constexpr size_t operator""_MB(unsigned long long int x) { return 1024_KB * x; }
constexpr size_t operator""_GB(unsigned long long int x) { return 1024_MB * x; }

81
include/http_server.hpp Normal file
View file

@ -0,0 +1,81 @@
#ifdef PANDA3DS_ENABLE_HTTP_SERVER
#pragma once
#include <array>
#include <atomic>
#include <condition_variable>
#include <filesystem>
#include <map>
#include <memory>
#include <mutex>
#include <queue>
#include <thread>
#include "helpers.hpp"
enum class HttpActionType { None, Screenshot, Key, TogglePause, Reset, LoadRom, Step };
class Emulator;
namespace httplib {
class Server;
struct Response;
}
// Wrapper for httplib::Response that allows the HTTP server to wait for the response to be ready
struct DeferredResponseWrapper {
DeferredResponseWrapper(httplib::Response& response) : inner_response(response) {}
httplib::Response& inner_response;
std::mutex mutex;
std::condition_variable cv;
bool ready = false;
};
// Actions derive from this class and are used to communicate with the HTTP server
class HttpAction {
HttpActionType type;
public:
HttpAction(HttpActionType type) : type(type) {}
virtual ~HttpAction() = default;
HttpActionType getType() const { return type; }
static std::unique_ptr<HttpAction> createScreenshotAction(DeferredResponseWrapper& response);
static std::unique_ptr<HttpAction> createKeyAction(u32 key, bool state);
static std::unique_ptr<HttpAction> createLoadRomAction(DeferredResponseWrapper& response, const std::filesystem::path& path, bool paused);
static std::unique_ptr<HttpAction> createTogglePauseAction();
static std::unique_ptr<HttpAction> createResetAction();
static std::unique_ptr<HttpAction> createStepAction(DeferredResponseWrapper& response, int frames);
};
struct HttpServer {
HttpServer(Emulator* emulator);
~HttpServer();
void processActions();
private:
static constexpr const char* httpServerScreenshotPath = "screenshot.png";
Emulator* emulator;
std::unique_ptr<httplib::Server> server;
std::thread httpServerThread;
std::queue<std::unique_ptr<HttpAction>> actionQueue;
std::mutex actionQueueMutex;
std::unique_ptr<HttpAction> currentStepAction {};
std::map<std::string, u32> keyMap;
bool paused = false;
int framesToRun = 0;
void startHttpServer();
void pushAction(std::unique_ptr<HttpAction> action);
std::string status();
u32 stringToKey(const std::string& key_name);
HttpServer(const HttpServer&) = delete;
HttpServer& operator=(const HttpServer&) = delete;
};
#endif // PANDA3DS_ENABLE_HTTP_SERVER

View file

@ -1,36 +0,0 @@
#ifdef PANDA3DS_ENABLE_HTTP_SERVER
#pragma once
#include <array>
#include <atomic>
#include <map>
#include <mutex>
#include "helpers.hpp"
enum class HttpAction { None, Screenshot, PressKey, ReleaseKey };
struct HttpServer {
static constexpr const char* httpServerScreenshotPath = "screenshot.png";
std::atomic_bool pendingAction = false;
HttpAction action = HttpAction::None;
std::mutex actionMutex = {};
u32 pendingKey = 0;
HttpServer();
void startHttpServer();
std::string status();
private:
std::map<std::string, std::pair<u32, bool>> keyMap;
std::array<bool, 12> pressedKeys = {};
bool paused = false;
u32 stringToKey(const std::string& key_name);
bool getKeyState(const std::string& key_name);
void setKeyState(const std::string& key_name, bool state);
};
#endif // PANDA3DS_ENABLE_HTTP_SERVER

View file

@ -27,6 +27,7 @@ class IOFile {
bool seek(std::int64_t offset, int origin = SEEK_SET);
bool rewind();
bool flush();
FILE* getHandle();
static void setAppDataDir(const std::filesystem::path& dir);
static std::filesystem::path getAppData() { return appData; }

View file

@ -9,10 +9,18 @@ namespace ConfigMem {
SyscoreVer = 0x1FF80010,
EnvInfo = 0x1FF80014,
AppMemAlloc = 0x1FF80040,
FirmUnknown = 0x1FF80060,
FirmRevision = 0x1FF80061,
FirmVersionMinor = 0x1FF80062,
FirmVersionMajor = 0x1FF80063,
FirmSyscoreVer = 0x1FF80064,
FirmSdkVer = 0x1FF80068,
HardwareType = 0x1FF81004,
Datetime0 = 0x1FF81020,
WifiMac = 0x1FF81060,
NetworkState = 0x1FF81067,
SliderState3D = 0x1FF81080,
LedState3D = 0x1FF81084,
BatteryState = 0x1FF81085,
Unknown1086 = 0x1FF81086,

View file

@ -17,22 +17,28 @@ namespace KernelHandles {
BOSS, // Streetpass stuff?
CAM, // Camera service
CECD, // More Streetpass stuff?
CFG, // CFG service (Console & region info)
DLP_SRVR, // Download Play: Server. Used for network play.
DSP, // DSP service (Used for audio decoding and output)
HID, // HID service (Handles everything input-related including gyro)
IR_USER, // One of 3 infrared communication services
FRD, // Friend service (Miiverse friend service)
FS, // Filesystem service
GPU, // GPU service
LCD, // LCD service (Used for configuring the displays)
LDR_RO, // Loader service. Used for loading CROs.
MIC, // MIC service (Controls the microphone)
NFC, // NFC (Duh), used for Amiibo
NIM, // Updates, DLC, etc
NDM, // ?????
PTM, // PTM service (Used for accessing various console info, such as battery, shell and pedometer state)
Y2R, // Also does camera stuff
CFG_U, // CFG service (Console & region info)
CFG_I,
DLP_SRVR, // Download Play: Server. Used for network play.
DSP, // DSP service (Used for audio decoding and output)
HID, // HID service (Handles input-related things including gyro. Does NOT handle New3DS controls or CirclePadPro)
HTTP, // HTTP service (Handles HTTP requests)
IR_USER, // One of 3 infrared communication services
FRD, // Friend service (Miiverse friend service)
FS, // Filesystem service
GPU, // GPU service
LCD, // LCD service (Used for configuring the displays)
LDR_RO, // Loader service. Used for loading CROs.
MCU_HWC, // Used for various MCU hardware-related things like battery control
MIC, // MIC service (Controls the microphone)
NFC, // NFC (Duh), used for Amiibo
NIM, // Updates, DLC, etc
NDM, // ?????
NEWS_U, // This service literally has 1 command (AddNotification) and I don't even understand what it does
PTM, // PTM service (Used for accessing various console info, such as battery, shell and pedometer state)
SOC, // Socket service
SSL, // SSL service (Totally didn't expect that)
Y2R, // Also does camera stuff
MinServiceHandle = AC,
MaxServiceHandle = Y2R,
@ -65,21 +71,27 @@ namespace KernelHandles {
case BOSS: return "BOSS";
case CAM: return "CAM";
case CECD: return "CECD";
case CFG: return "CFG";
case CFG_U: return "CFG:U";
case CFG_I: return "CFG:I";
case DSP: return "DSP";
case DLP_SRVR: return "DLP::SRVR";
case HID: return "HID";
case HTTP: return "HTTP";
case IR_USER: return "IR:USER";
case FRD: return "FRD";
case FS: return "FS";
case GPU: return "GSP::GPU";
case LCD: return "GSP::LCD";
case LDR_RO: return "LDR:RO";
case MCU_HWC: return "MCU::HWC";
case MIC: return "MIC";
case NDM: return "NDM";
case NEWS_U: return "NEWS_U";
case NFC: return "NFC";
case NIM: return "NIM";
case PTM: return "PTM";
case SOC: return "SOC";
case SSL: return "SSL";
case Y2R: return "Y2R";
default: return "Unknown";
}

View file

@ -6,6 +6,7 @@
#include <string>
#include <vector>
#include "config.hpp"
#include "helpers.hpp"
#include "kernel_types.hpp"
#include "logger.hpp"
@ -34,6 +35,7 @@ class Kernel {
std::vector<KernelObject> objects;
std::vector<Handle> portHandles;
std::vector<Handle> mutexHandles;
// Thread indices, sorted by priority
std::vector<int> threadIndices;
@ -52,17 +54,21 @@ class Kernel {
// Top 8 bits are the major version, bottom 8 are the minor version
u16 kernelVersion = 0;
// Shows whether a reschedule will be need
bool needReschedule = false;
Handle makeArbiter();
Handle makeProcess(u32 id);
Handle makePort(const char* name);
Handle makeSession(Handle port);
Handle makeThread(u32 entrypoint, u32 initialSP, u32 priority, s32 id, u32 arg,ThreadStatus status = ThreadStatus::Dormant);
Handle makeThread(u32 entrypoint, u32 initialSP, u32 priority, ProcessorID id, u32 arg,ThreadStatus status = ThreadStatus::Dormant);
Handle makeMemoryBlock(u32 addr, u32 size, u32 myPermission, u32 otherPermission);
public:
Handle makeEvent(ResetType resetType); // Needs to be public to be accessible to the APT/HID services
Handle makeMutex(bool locked = false); // Needs to be public to be accessible to the APT/DSP services
Handle makeSemaphore(u32 initialCount, u32 maximumCount); // Needs to be public to be accessible to the service manager port
Handle makeTimer(ResetType resetType);
// Signals an event, returns true on success or false if the event does not exist
bool signalEvent(Handle e);
@ -73,7 +79,6 @@ private:
void switchThread(int newThreadIndex);
void sortThreads();
std::optional<int> getNextThread();
void switchToNextThread();
void rescheduleThreads();
bool canThreadRun(const Thread& t);
bool shouldWaitOnObject(KernelObject* object);
@ -107,9 +112,9 @@ private:
MAKE_LOG_FUNCTION(log, kernelLogger)
MAKE_LOG_FUNCTION(logSVC, svcLogger)
MAKE_LOG_FUNCTION(logThread, threadLogger)
MAKE_LOG_FUNCTION(logDebugString, debugStringLogger)
MAKE_LOG_FUNCTION(logError, errorLogger)
MAKE_LOG_FUNCTION(logFileIO, fileIOLogger)
MAKE_LOG_FUNCTION_USER(logDebugString, debugStringLogger)
// SVC implementations
void arbitrateAddress();
@ -121,24 +126,31 @@ private:
void exitThread();
void mapMemoryBlock();
void queryMemory();
void getCurrentProcessorNumber();
void getProcessID();
void getProcessInfo();
void getResourceLimit();
void getResourceLimitLimitValues();
void getResourceLimitCurrentValues();
void getSystemInfo();
void getSystemTick();
void getThreadID();
void getThreadIdealProcessor();
void getThreadPriority();
void sendSyncRequest();
void setThreadPriority();
void svcCancelTimer();
void svcClearEvent();
void svcClearTimer();
void svcCloseHandle();
void svcCreateEvent();
void svcCreateMutex();
void svcCreateSemaphore();
void svcCreateTimer();
void svcReleaseMutex();
void svcReleaseSemaphore();
void svcSignalEvent();
void svcSetTimer();
void svcSleepThread();
void connectToPort();
void outputDebugString();
@ -162,12 +174,21 @@ private:
void readDirectory(u32 messagePointer, Handle directory);
public:
Kernel(CPU& cpu, Memory& mem, GPU& gpu);
Kernel(CPU& cpu, Memory& mem, GPU& gpu, const EmulatorConfig& config);
void initializeFS() { return serviceManager.initializeFS(); }
void setVersion(u8 major, u8 minor);
void serviceSVC(u32 svc);
void reset();
void requireReschedule() { needReschedule = true; }
void evalReschedule() {
if (needReschedule) {
needReschedule = false;
rescheduleThreads();
}
}
Handle makeObject(KernelObjectType type) {
if (handleCounter > KernelHandles::Max) [[unlikely]] {
Helpers::panic("Hlep we somehow created enough kernel objects to overflow this thing");

View file

@ -34,6 +34,16 @@ enum class ArbitrationType {
DecrementAndWaitIfLessTimeout = 4
};
enum class ProcessorID : s32 {
AllCPUs = -1,
Default = -2,
AppCore = 0,
Syscore = 1,
New3DSExtra1 = 2,
New3DSExtra2 = 3
};
struct AddressArbiter {};
struct ResourceLimits {
@ -95,7 +105,7 @@ struct Thread {
u32 entrypoint; // Initial r15 value
u32 priority;
u32 arg;
s32 processorID;
ProcessorID processorID;
ThreadStatus status;
Handle handle; // OS handle for this thread
int index; // Index of the thread. 0 for the first thread, 1 for the second, and so on

View file

@ -2,77 +2,84 @@
#include <array>
#include <optional>
#include <vector>
#include "io_file.hpp"
#include "helpers.hpp"
#include "crypto/aes_engine.hpp"
#include "helpers.hpp"
#include "io_file.hpp"
#include "services/region_codes.hpp"
struct NCCH {
struct EncryptionInfo {
Crypto::AESKey normalKey;
Crypto::AESKey initialCounter;
};
struct EncryptionInfo {
Crypto::AESKey normalKey;
Crypto::AESKey initialCounter;
};
struct FSInfo { // Info on the ExeFS/RomFS
u64 offset = 0;
u64 size = 0;
u64 hashRegionSize = 0;
std::optional<EncryptionInfo> encryptionInfo;
};
struct FSInfo { // Info on the ExeFS/RomFS
u64 offset = 0;
u64 size = 0;
u64 hashRegionSize = 0;
std::optional<EncryptionInfo> encryptionInfo;
};
// Descriptions for .text, .data and .rodata sections
struct CodeSetInfo {
u32 address = 0;
u32 pageCount = 0;
u32 size = 0;
// Descriptions for .text, .data and .rodata sections
struct CodeSetInfo {
u32 address = 0;
u32 pageCount = 0;
u32 size = 0;
// Extract the code set info from the relevant header data
void extract(const u8* headerEntry) {
address = *(u32*)&headerEntry[0];
pageCount = *(u32*)&headerEntry[4];
size = *(u32*)&headerEntry[8];
}
};
// Extract the code set info from the relevant header data
void extract(const u8 *headerEntry) {
address = *(u32 *)&headerEntry[0];
pageCount = *(u32 *)&headerEntry[4];
size = *(u32 *)&headerEntry[8];
}
};
u64 partitionIndex = 0;
u64 fileOffset = 0;
u64 partitionIndex = 0;
u64 fileOffset = 0;
bool isNew3DS = false;
bool initialized = false;
bool compressCode = false; // Shows whether the .code file in the ExeFS is compressed
bool mountRomFS = false;
bool encrypted = false;
bool fixedCryptoKey = false;
bool seedCrypto = false;
u8 secondaryKeySlot = 0;
bool isNew3DS = false;
bool initialized = false;
bool compressCode = false; // Shows whether the .code file in the ExeFS is compressed
bool mountRomFS = false;
bool encrypted = false;
bool fixedCryptoKey = false;
bool seedCrypto = false;
u8 secondaryKeySlot = 0;
static constexpr u64 mediaUnit = 0x200;
u64 size = 0; // Size of NCCH converted to bytes
u32 stackSize = 0;
u32 bssSize = 0;
u32 exheaderSize = 0;
static constexpr u64 mediaUnit = 0x200;
u64 size = 0; // Size of NCCH converted to bytes
u32 stackSize = 0;
u32 bssSize = 0;
u32 exheaderSize = 0;
FSInfo exheaderInfo;
FSInfo exeFS;
FSInfo romFS;
CodeSetInfo text, data, rodata;
FSInfo exheaderInfo;
FSInfo exeFS;
FSInfo romFS;
CodeSetInfo text, data, rodata;
// Contents of the .code file in the ExeFS
std::vector<u8> codeFile;
// Contains of the cart's save data
std::vector<u8> saveData;
// Contents of the .code file in the ExeFS
std::vector<u8> codeFile;
// Contains of the cart's save data
std::vector<u8> saveData;
// The cart region. Only the CXI's region matters to us. Necessary to get past region locking
std::optional<Regions> region = std::nullopt;
// Returns true on success, false on failure
// Partition index/offset/size must have been set before this
bool loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSInfo &info);
// Returns true on success, false on failure
// Partition index/offset/size must have been set before this
bool loadFromHeader(Crypto::AESEngine &aesEngine, IOFile &file, const FSInfo &info);
bool hasExtendedHeader() { return exheaderSize != 0; }
bool hasExeFS() { return exeFS.size != 0; }
bool hasRomFS() { return romFS.size != 0; }
bool hasCode() { return codeFile.size() != 0; }
bool hasSaveData() { return saveData.size() != 0; }
bool hasExtendedHeader() { return exheaderSize != 0; }
bool hasExeFS() { return exeFS.size != 0; }
bool hasRomFS() { return romFS.size != 0; }
bool hasCode() { return codeFile.size() != 0; }
bool hasSaveData() { return saveData.size() != 0; }
std::pair<bool, Crypto::AESKey> getPrimaryKey(Crypto::AESEngine &aesEngine, const Crypto::AESKey &keyY);
std::pair<bool, Crypto::AESKey> getSecondaryKey(Crypto::AESEngine &aesEngine, const Crypto::AESKey &keyY);
// Parse SMDH for region info and such. Returns false on failure, true on success
bool parseSMDH(const std::vector<u8> &smdh);
std::pair<bool, std::size_t> readFromFile(IOFile& file, const FSInfo &info, u8 *dst, std::size_t offset, std::size_t size);
std::pair<bool, Crypto::AESKey> getPrimaryKey(Crypto::AESEngine &aesEngine, const Crypto::AESKey &keyY);
std::pair<bool, Crypto::AESKey> getSecondaryKey(Crypto::AESEngine &aesEngine, const Crypto::AESKey &keyY);
std::pair<bool, std::size_t> readFromFile(IOFile &file, const FSInfo &info, u8 *dst, std::size_t offset, std::size_t size);
};

View file

@ -3,60 +3,80 @@
#include <fstream>
namespace Log {
// Our logger class
template <bool enabled>
class Logger {
public:
void log(const char* fmt, ...) {
if constexpr (!enabled) return;
// Our logger class
template <bool enabled>
class Logger {
public:
void log(const char* fmt, ...) {
if constexpr (!enabled) return;
std::va_list args;
va_start(args, fmt);
std::vprintf(fmt, args);
va_end(args);
}
};
std::va_list args;
va_start(args, fmt);
std::vprintf(fmt, args);
va_end(args);
}
};
// Our loggers here. Enable/disable by toggling the template param
static Logger<false> kernelLogger;
static Logger<true> debugStringLogger; // Enables output for the outputDebugString SVC
static Logger<false> errorLogger;
static Logger<false> fileIOLogger;
static Logger<false> svcLogger;
static Logger<false> threadLogger;
static Logger<false> gpuLogger;
static Logger<false> rendererLogger;
static Logger<false> shaderJITLogger;
// Our loggers here. Enable/disable by toggling the template param
static Logger<false> kernelLogger;
// Enables output for the outputDebugString SVC
static Logger<true> debugStringLogger;
static Logger<false> errorLogger;
static Logger<false> fileIOLogger;
static Logger<false> svcLogger;
static Logger<false> threadLogger;
static Logger<false> gpuLogger;
static Logger<false> rendererLogger;
static Logger<false> shaderJITLogger;
// Service loggers
static Logger<false> acLogger;
static Logger<false> actLogger;
static Logger<false> amLogger;
static Logger<false> aptLogger;
static Logger<false> bossLogger;
static Logger<false> camLogger;
static Logger<false> cecdLogger;
static Logger<false> cfgLogger;
static Logger<false> dspServiceLogger;
static Logger<false> dlpSrvrLogger;
static Logger<false> frdLogger;
static Logger<false> fsLogger;
static Logger<false> hidLogger;
// Service loggers
static Logger<false> acLogger;
static Logger<false> actLogger;
static Logger<false> amLogger;
static Logger<false> aptLogger;
static Logger<false> bossLogger;
static Logger<false> camLogger;
static Logger<false> cecdLogger;
static Logger<false> cfgLogger;
static Logger<false> dspServiceLogger;
static Logger<false> dlpSrvrLogger;
static Logger<false> frdLogger;
static Logger<false> fsLogger;
static Logger<false> hidLogger;
static Logger<false> httpLogger;
static Logger<false> irUserLogger;
static Logger<false> gspGPULogger;
static Logger<false> gspLCDLogger;
static Logger<false> ldrLogger;
static Logger<false> micLogger;
static Logger<false> nfcLogger;
static Logger<false> nimLogger;
static Logger<false> ndmLogger;
static Logger<false> ptmLogger;
static Logger<false> y2rLogger;
static Logger<false> srvLogger;
static Logger<false> gspGPULogger;
static Logger<false> gspLCDLogger;
static Logger<false> ldrLogger;
static Logger<false> mcuLogger;
static Logger<false> micLogger;
static Logger<false> newsLogger;
static Logger<false> nfcLogger;
static Logger<false> nimLogger;
static Logger<false> ndmLogger;
static Logger<false> ptmLogger;
static Logger<false> socLogger;
static Logger<false> sslLogger;
static Logger<false> y2rLogger;
static Logger<false> srvLogger;
#define MAKE_LOG_FUNCTION(functionName, logger) \
template <typename... Args> \
void functionName(const char* fmt, Args... args) { \
Log::logger.log(fmt, args...); \
}
// We have 2 ways to create a log function
// MAKE_LOG_FUNCTION: Creates a log function which is toggleable but always killed for user-facing builds
// MAKE_LOG_FUNCTION_USER: Creates a log function which is toggleable, may be on for user builds as well
// We need this because sadly due to the loggers taking variadic arguments, compilers will not properly
// Kill them fully even when they're disabled. The only way they will is if the function with varargs is totally empty
#define MAKE_LOG_FUNCTION_USER(functionName, logger) \
template <typename... Args> \
void functionName(const char* fmt, Args&&... args) { \
Log::logger.log(fmt, args...); \
}
#ifdef PANDA3DS_USER_BUILD
#define MAKE_LOG_FUNCTION(functionName, logger) \
template <typename... Args> \
void functionName(const char* fmt, Args&&... args) {}
#else
#define MAKE_LOG_FUNCTION(functionName, logger) MAKE_LOG_FUNCTION_USER(functionName, logger)
#endif
}

73
include/math_util.hpp Normal file
View file

@ -0,0 +1,73 @@
// Copyright 2013 Dolphin Emulator Project / 2014 Citra Emulator Project / 2023 Panda3DS Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <cstdlib>
#include <type_traits>
namespace Math {
template <class T>
struct Rectangle {
T left{};
T top{};
T right{};
T bottom{};
constexpr Rectangle() = default;
constexpr Rectangle(T left, T top, T right, T bottom)
: left(left), top(top), right(right), bottom(bottom) {}
[[nodiscard]] constexpr bool operator==(const Rectangle<T>& rhs) const {
return (left == rhs.left) && (top == rhs.top) && (right == rhs.right) &&
(bottom == rhs.bottom);
}
[[nodiscard]] constexpr bool operator!=(const Rectangle<T>& rhs) const {
return !operator==(rhs);
}
[[nodiscard]] constexpr Rectangle<T> operator*(const T value) const {
return Rectangle{left * value, top * value, right * value, bottom * value};
}
[[nodiscard]] constexpr Rectangle<T> operator/(const T value) const {
return Rectangle{left / value, top / value, right / value, bottom / value};
}
[[nodiscard]] T getWidth() const {
return std::abs(static_cast<std::make_signed_t<T>>(right - left));
}
[[nodiscard]] T getHeight() const {
return std::abs(static_cast<std::make_signed_t<T>>(bottom - top));
}
[[nodiscard]] T getArea() const {
return getWidth() * getHeight();
}
[[nodiscard]] Rectangle<T> translateX(const T x) const {
return Rectangle{left + x, top, right + x, bottom};
}
[[nodiscard]] Rectangle<T> translateY(const T y) const {
return Rectangle{left, top + y, right, bottom + y};
}
[[nodiscard]] Rectangle<T> scale(const float s) const {
return Rectangle{left, top, static_cast<T>(left + getWidth() * s),
static_cast<T>(top + getHeight() * s)};
}
};
template <typename T>
Rectangle(T, T, T, T) -> Rectangle<T>;
template <typename T>
using Rect = Rectangle<T>;
} // end namespace Math

View file

@ -5,11 +5,13 @@
#include <fstream>
#include <optional>
#include <vector>
#include "config.hpp"
#include "crypto/aes_engine.hpp"
#include "helpers.hpp"
#include "handles.hpp"
#include "helpers.hpp"
#include "loader/ncsd.hpp"
#include "services/shared_font.hpp"
#include "services/region_codes.hpp"
namespace PhysicalAddrs {
enum : u32 {
@ -110,7 +112,7 @@ class Memory {
std::vector<KernelMemoryTypes::MemoryInfo> memoryInfo;
std::array<SharedMemoryBlock, 3> sharedMemBlocks = {
SharedMemoryBlock(0, u32(_shared_font_len), KernelHandles::FontSharedMemHandle), // Shared memory for the system font
SharedMemoryBlock(0, 0, KernelHandles::FontSharedMemHandle), // Shared memory for the system font (size is 0 because we read the size from the cmrc filesystem
SharedMemoryBlock(0, 0x1000, KernelHandles::GSPSharedMemHandle), // GSP shared memory
SharedMemoryBlock(0, 0x1000, KernelHandles::HIDSharedMemHandle) // HID shared memory
};
@ -139,12 +141,28 @@ private:
// Report a retail unit without JTAG
static constexpr u32 envInfo = 1;
// Stored in Configuration Memory starting @ 0x1FF80060
struct FirmwareInfo {
u8 unk; // Usually 0 according to 3DBrew
u8 revision;
u8 minor;
u8 major;
u32 syscoreVer;
u32 sdkVer;
};
// Values taken from 3DBrew and Citra
static constexpr FirmwareInfo firm{.unk = 0, .revision = 0, .minor = 0x34, .major = 2, .syscoreVer = 2, .sdkVer = 0x0000F297};
// Adjusted upon loading a ROM based on the ROM header. Used by CFG::SecureInfoGetArea to get past region locks
Regions region = Regions::USA;
const EmulatorConfig& config;
public:
u16 kernelVersion = 0;
u32 usedUserMemory = u32(0_MB); // How much of the APPLICATION FCRAM range is used (allocated to the appcore)
u32 usedSystemMemory = u32(0_MB); // Similar for the SYSTEM range (reserved for the syscore)
Memory(u64& cpuTicks);
Memory(u64& cpuTicks, const EmulatorConfig& config);
void reset();
void* getReadPointer(u32 address);
void* getWritePointer(u32 address);
@ -248,4 +266,6 @@ public:
void setVRAM(u8* pointer) { vram = pointer; }
bool allocateMainThreadStack(u32 size);
Regions getConsoleRegion();
void copySharedFont(u8* ptr);
};

View file

@ -1,18 +1,31 @@
#pragma once
#include <array>
#include <span>
#include <optional>
#include "PICA/pica_vertex.hpp"
#include "PICA/regs.hpp"
#include "helpers.hpp"
enum class RendererType : s8 {
// Todo: Auto = -1,
Null = 0,
OpenGL = 1,
Vulkan = 2,
Software = 3,
};
class GPU;
struct SDL_Window;
class Renderer {
protected:
GPU& gpu;
static constexpr u32 regNum = 0x300; // Number of internal PICA registers
static constexpr u32 regNum = 0x300; // Number of internal PICA registers
static constexpr u32 extRegNum = 0x1000; // Number of external PICA registers
const std::array<u32, regNum>& regs;
const std::array<u32, extRegNum>& externalRegs;
std::array<u32, 2> fbSize; // The size of the framebuffer (ie both the colour and depth buffer)'
@ -24,16 +37,19 @@ class Renderer {
PICA::DepthFmt depthBufferFormat;
public:
Renderer(GPU& gpu, const std::array<u32, regNum>& internalRegs);
Renderer(GPU& gpu, const std::array<u32, regNum>& internalRegs, const std::array<u32, extRegNum>& externalRegs);
virtual ~Renderer();
static constexpr u32 vertexBufferSize = 0x10000;
static std::optional<RendererType> typeFromString(std::string inString);
static const char* typeToString(RendererType rendererType);
virtual void reset() = 0;
virtual void display() = 0; // Display the 3DS screen contents to the window
virtual void initGraphicsContext() = 0; // Initialize graphics context
virtual void initGraphicsContext(SDL_Window* window) = 0; // Initialize graphics context
virtual void clearBuffer(u32 startAddress, u32 endAddress, u32 value, u32 control) = 0; // Clear a GPU buffer in VRAM
virtual void displayTransfer(u32 inputAddr, u32 outputAddr, u32 inputSize, u32 outputSize, u32 flags) = 0; // Perform display transfer
virtual void textureCopy(u32 inputAddr, u32 outputAddr, u32 totalBytes, u32 inputSize, u32 outputSize, u32 flags) = 0;
virtual void drawVertices(PICA::PrimType primType, std::span<const PICA::Vertex> vertices) = 0; // Draw the given vertices
virtual void screenshot(const std::string& name) = 0;

View file

@ -1,6 +1,7 @@
#pragma once
#include <type_traits>
#include "helpers.hpp"
#include "opengl.hpp"
// GL state manager object for use in the OpenGL GPU renderer and potentially other things in the future (such as a potential ImGui GUI)
@ -18,28 +19,42 @@
// backend-agnostic as possible
struct GLStateManager {
// We only support 6 clipping planes in our state manager because that's the minimum for GL_MAX_CLIP_PLANES
// And nobody needs more than 6 clip planes anyways
static constexpr GLint clipPlaneCount = 6;
bool blendEnabled;
bool logicOpEnabled;
bool depthEnabled;
bool scissorEnabled;
bool stencilEnabled;
u32 enabledClipPlanes; // Bitfield of enabled clip planes
// Colour/depth masks
bool redMask, greenMask, blueMask, alphaMask;
bool depthMask;
float clearRed, clearBlue, clearGreen, clearAlpha;
GLuint stencilMask;
GLuint boundVAO;
GLuint boundVBO;
GLuint currentProgram;
GLenum depthFunc;
GLenum logicOp;
void reset();
void resetBlend();
void resetClearing();
void resetClipping();
void resetColourMask();
void resetDepth();
void resetVAO();
void resetVBO();
void resetProgram();
void resetScissor();
void resetStencil();
void enableDepth() {
if (!depthEnabled) {
@ -83,6 +98,70 @@ struct GLStateManager {
}
}
void enableStencil() {
if (!stencilEnabled) {
stencilEnabled = true;
OpenGL::enableStencil();
}
}
void disableStencil() {
if (stencilEnabled) {
stencilEnabled = false;
OpenGL::disableStencil();
}
}
void enableLogicOp() {
if (!logicOpEnabled) {
logicOpEnabled = true;
OpenGL::enableLogicOp();
}
}
void disableLogicOp() {
if (logicOpEnabled) {
logicOpEnabled = false;
OpenGL::disableLogicOp();
}
}
void setLogicOp(GLenum op) {
if (logicOp != op) {
logicOp = op;
OpenGL::setLogicOp(op);
}
}
void enableClipPlane(GLuint index) {
if (index >= clipPlaneCount) [[unlikely]] {
Helpers::panic("Enabled invalid clipping plane %d\n", index);
}
if ((enabledClipPlanes & (1 << index)) == 0) {
enabledClipPlanes |= 1 << index; // Enable relevant bit in clipping plane bitfield
OpenGL::enableClipPlane(index); // Enable plane
}
}
void disableClipPlane(GLuint index) {
if (index >= clipPlaneCount) [[unlikely]] {
Helpers::panic("Disabled invalid clipping plane %d\n", index);
}
if ((enabledClipPlanes & (1 << index)) != 0) {
enabledClipPlanes ^= 1 << index; // Disable relevant bit in bitfield by flipping it
OpenGL::disableClipPlane(index); // Disable plane
}
}
void setStencilMask(GLuint mask) {
if (stencilMask != mask) {
stencilMask = mask;
OpenGL::setStencilMask(mask);
}
}
void bindVAO(GLuint handle) {
if (boundVAO != handle) {
boundVAO = handle;
@ -133,6 +212,17 @@ struct GLStateManager {
}
}
void setClearColour(float r, float g, float b, float a) {
if (clearRed != r || clearGreen != g || clearBlue != b || clearAlpha != a) {
clearRed = r;
clearGreen = g;
clearBlue = b;
clearAlpha = a;
OpenGL::setClearColor(r, g, b, a);
}
}
void setDepthFunc(OpenGL::DepthFunc func) { setDepthFunc(static_cast<GLenum>(func)); }
};

View file

@ -44,8 +44,8 @@ class RendererGL final : public Renderer {
float oldDepthOffset = 0.0;
bool oldDepthmapEnable = false;
SurfaceCache<DepthBuffer, 10, true> depthBufferCache;
SurfaceCache<ColourBuffer, 10, true> colourBufferCache;
SurfaceCache<DepthBuffer, 16, true> depthBufferCache;
SurfaceCache<ColourBuffer, 16, true> colourBufferCache;
SurfaceCache<Texture, 256, true> textureCache;
// Dummy VAO/VBO for blitting the final output
@ -55,27 +55,34 @@ class RendererGL final : public Renderer {
OpenGL::Texture screenTexture;
GLuint lightLUTTextureArray;
OpenGL::Framebuffer screenFramebuffer;
OpenGL::Texture blankTexture;
OpenGL::Framebuffer getColourFBO();
OpenGL::Texture getTexture(Texture& tex);
MAKE_LOG_FUNCTION(log, rendererLogger)
void setupBlending();
void setupStencilTest(bool stencilEnable);
void bindDepthBuffer();
void setupTextureEnvState();
void bindTexturesToSlots();
void updateLightingLUT();
public:
RendererGL(GPU& gpu, const std::array<u32, regNum>& internalRegs) : Renderer(gpu, internalRegs) {}
RendererGL(GPU& gpu, const std::array<u32, regNum>& internalRegs, const std::array<u32, extRegNum>& externalRegs)
: Renderer(gpu, internalRegs, externalRegs) {}
~RendererGL() override;
void reset() override;
void display() override; // Display the 3DS screen contents to the window
void initGraphicsContext() override; // Initialize graphics context
void initGraphicsContext(SDL_Window* window) override; // Initialize graphics context
void clearBuffer(u32 startAddress, u32 endAddress, u32 value, u32 control) override; // Clear a GPU buffer in VRAM
void displayTransfer(u32 inputAddr, u32 outputAddr, u32 inputSize, u32 outputSize, u32 flags) override; // Perform display transfer
void textureCopy(u32 inputAddr, u32 outputAddr, u32 totalBytes, u32 inputSize, u32 outputSize, u32 flags) override;
void drawVertices(PICA::PrimType primType, std::span<const PICA::Vertex> vertices) override; // Draw the given vertices
std::optional<ColourBuffer> getColourBuffer(u32 addr, PICA::ColorFmt format, u32 width, u32 height, bool createIfnotFound = true);
// Take a screenshot of the screen and store it in a file
void screenshot(const std::string& name) override;
};

View file

@ -76,6 +76,16 @@ public:
size++;
// Find an existing surface we completely invalidate and overwrite it with the new surface
for (auto& e : buffer) {
if (e.valid && e.range.lower() >= surface.range.lower() && e.range.upper() <= surface.range.upper()) {
e.free();
e = surface;
e.allocate();
return e;
}
}
// Find an invalid entry in the cache and overwrite it with the new surface
for (auto& e : buffer) {
if (!e.valid) {

View file

@ -2,62 +2,70 @@
#include "PICA/regs.hpp"
#include "boost/icl/interval.hpp"
#include "helpers.hpp"
#include "math_util.hpp"
#include "opengl.hpp"
template <typename T>
using Interval = boost::icl::right_open_interval<T>;
struct ColourBuffer {
u32 location;
PICA::ColorFmt format;
OpenGL::uvec2 size;
bool valid;
u32 location;
PICA::ColorFmt format;
OpenGL::uvec2 size;
bool valid;
// Range of VRAM taken up by buffer
Interval<u32> range;
// OpenGL resources allocated to buffer
OpenGL::Texture texture;
OpenGL::Framebuffer fbo;
// Range of VRAM taken up by buffer
Interval<u32> range;
// OpenGL resources allocated to buffer
OpenGL::Texture texture;
OpenGL::Framebuffer fbo;
ColourBuffer() : valid(false) {}
ColourBuffer() : valid(false) {}
ColourBuffer(u32 loc, PICA::ColorFmt format, u32 x, u32 y, bool valid = true)
: location(loc), format(format), size({x, y}), valid(valid) {
ColourBuffer(u32 loc, PICA::ColorFmt format, u32 x, u32 y, bool valid = true) : location(loc), format(format), size({x, y}), valid(valid) {
u64 endLoc = (u64)loc + sizeInBytes();
// Check if start and end are valid here
range = Interval<u32>(loc, (u32)endLoc);
}
u64 endLoc = (u64)loc + sizeInBytes();
// Check if start and end are valid here
range = Interval<u32>(loc, (u32)endLoc);
}
void allocate() {
// Create texture for the FBO, setting up filters and the like
// Reading back the current texture is slow, but allocate calls should be few and far between.
// If this becomes a bottleneck, we can fix it semi-easily
auto prevTexture = OpenGL::getTex2D();
texture.create(size.x(), size.y(), GL_RGBA8);
texture.bind();
texture.setMinFilter(OpenGL::Linear);
texture.setMagFilter(OpenGL::Linear);
glBindTexture(GL_TEXTURE_2D, prevTexture);
void allocate() {
// Create texture for the FBO, setting up filters and the like
// Reading back the current texture is slow, but allocate calls should be few and far between.
// If this becomes a bottleneck, we can fix it semi-easily
auto prevTexture = OpenGL::getTex2D();
texture.create(size.x(), size.y(), GL_RGBA8);
texture.bind();
texture.setMinFilter(OpenGL::Linear);
texture.setMagFilter(OpenGL::Linear);
glBindTexture(GL_TEXTURE_2D, prevTexture);
#ifdef GPU_DEBUG_INFO
const auto name = Helpers::format("Surface %dx%d %s from 0x%08X", size.x(), size.y(), PICA::textureFormatToString(format), location);
OpenGL::setObjectLabel(GL_TEXTURE, texture.handle(), name.c_str());
#endif
//Helpers::panic("Creating FBO: %d, %d\n", size.x(), size.y());
fbo.createWithDrawTexture(texture);
fbo.bind(OpenGL::DrawAndReadFramebuffer);
fbo.createWithDrawTexture(texture);
fbo.bind(OpenGL::DrawAndReadFramebuffer);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
Helpers::panic("Incomplete framebuffer");
}
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
Helpers::panic("Incomplete framebuffer");
// TODO: This should not clear the framebuffer contents. It should load them from VRAM.
GLint oldViewport[4];
GLfloat oldClearColour[4];
// TODO: This should not clear the framebuffer contents. It should load them from VRAM.
GLint oldViewport[4];
glGetIntegerv(GL_VIEWPORT, oldViewport);
OpenGL::setViewport(size.x(), size.y());
OpenGL::setClearColor(0.0, 0.0, 0.0, 1.0);
OpenGL::clearColor();
OpenGL::setViewport(oldViewport[0], oldViewport[1], oldViewport[2], oldViewport[3]);
}
glGetIntegerv(GL_VIEWPORT, oldViewport);
glGetFloatv(GL_COLOR_CLEAR_VALUE, oldClearColour);
void free() {
OpenGL::setViewport(size.x(), size.y());
OpenGL::setClearColor(0.0, 0.0, 0.0, 1.0);
OpenGL::clearColor();
OpenGL::setViewport(oldViewport[0], oldViewport[1], oldViewport[2], oldViewport[3]);
OpenGL::setClearColor(oldClearColour[0], oldClearColour[1], oldClearColour[2], oldClearColour[3]);
}
void free() {
valid = false;
if (texture.exists() || fbo.exists()) {
@ -66,82 +74,102 @@ struct ColourBuffer {
}
}
bool matches(ColourBuffer& other) {
return location == other.location && format == other.format &&
size.x() == other.size.x() && size.y() == other.size.y();
}
Math::Rect<u32> getSubRect(u32 inputAddress, u32 width, u32 height) {
// PICA textures have top-left origin while OpenGL has bottom-left origin.
// Flip the rectangle on the x axis to account for this.
const u32 startOffset = (inputAddress - location) / sizePerPixel(format);
const u32 x0 = (startOffset % (size.x() * 8)) / 8;
const u32 y0 = (startOffset / (size.x() * 8)) * 8;
return Math::Rect<u32>{x0, size.y() - y0, x0 + width, size.y() - height - y0};
}
size_t sizeInBytes() {
return (size_t)size.x() * (size_t)size.y() * PICA::sizePerPixel(format);
}
bool matches(ColourBuffer& other) {
return location == other.location && format == other.format && size.x() == other.size.x() && size.y() == other.size.y();
}
size_t sizeInBytes() {
return (size_t)size.x() * (size_t)size.y() * PICA::sizePerPixel(format);
}
};
struct DepthBuffer {
u32 location;
PICA::DepthFmt format;
OpenGL::uvec2 size; // Implicitly set to the size of the framebuffer
bool valid;
u32 location;
PICA::DepthFmt format;
OpenGL::uvec2 size; // Implicitly set to the size of the framebuffer
bool valid;
// Range of VRAM taken up by buffer
Interval<u32> range;
// OpenGL texture used for storing depth/stencil
OpenGL::Texture texture;
// Range of VRAM taken up by buffer
Interval<u32> range;
// OpenGL texture used for storing depth/stencil
OpenGL::Texture texture;
OpenGL::Framebuffer fbo;
DepthBuffer() : valid(false) {}
DepthBuffer() : valid(false) {}
DepthBuffer(u32 loc, PICA::DepthFmt format, u32 x, u32 y, bool valid = true) :
location(loc), format(format), size({x, y}), valid(valid) {
DepthBuffer(u32 loc, PICA::DepthFmt format, u32 x, u32 y, bool valid = true) : location(loc), format(format), size({x, y}), valid(valid) {
u64 endLoc = (u64)loc + sizeInBytes();
// Check if start and end are valid here
range = Interval<u32>(loc, (u32)endLoc);
}
u64 endLoc = (u64)loc + sizeInBytes();
// Check if start and end are valid here
range = Interval<u32>(loc, (u32)endLoc);
}
void allocate() {
// Create texture for the FBO, setting up filters and the like
// Reading back the current texture is slow, but allocate calls should be few and far between.
// If this becomes a bottleneck, we can fix it semi-easily
auto prevTexture = OpenGL::getTex2D();
void allocate() {
// Create texture for the FBO, setting up filters and the like
// Reading back the current texture is slow, but allocate calls should be few and far between.
// If this becomes a bottleneck, we can fix it semi-easily
auto prevTexture = OpenGL::getTex2D();
// Internal formats for the texture based on format
static constexpr std::array<GLenum, 4> internalFormats = {
GL_DEPTH_COMPONENT16,
GL_DEPTH_COMPONENT24,
GL_DEPTH_COMPONENT24,
GL_DEPTH24_STENCIL8,
};
// Internal formats for the texture based on format
static constexpr std::array<GLenum, 4> internalFormats = {
GL_DEPTH_COMPONENT16, GL_DEPTH_COMPONENT24, GL_DEPTH_COMPONENT24, GL_DEPTH24_STENCIL8
};
// Format of the texture
static constexpr std::array<GLenum, 4> formats = {
GL_DEPTH_COMPONENT,
GL_DEPTH_COMPONENT,
GL_DEPTH_COMPONENT,
GL_DEPTH_STENCIL,
};
// Format of the texture
static constexpr std::array<GLenum, 4> formats = {
GL_DEPTH_COMPONENT, GL_DEPTH_COMPONENT, GL_DEPTH_COMPONENT, GL_DEPTH_STENCIL
};
static constexpr std::array<GLenum, 4> types = {
GL_UNSIGNED_SHORT,
GL_UNSIGNED_INT,
GL_UNSIGNED_INT,
GL_UNSIGNED_INT_24_8,
};
static constexpr std::array<GLenum, 4> types = {
GL_UNSIGNED_SHORT, GL_UNSIGNED_INT, GL_UNSIGNED_INT, GL_UNSIGNED_INT_24_8
};
auto internalFormat = internalFormats[(int)format];
auto fmt = formats[(int)format];
auto type = types[(int)format];
auto internalFormat = internalFormats[(int)format];
auto fmt = formats[(int)format];
auto type = types[(int)format];
texture.createDSTexture(size.x(), size.y(), internalFormat, fmt, nullptr, type, GL_TEXTURE_2D);
texture.bind();
texture.setMinFilter(OpenGL::Nearest);
texture.setMagFilter(OpenGL::Nearest);
texture.createDSTexture(size.x(), size.y(), internalFormat, fmt, nullptr, type, GL_TEXTURE_2D);
texture.bind();
texture.setMinFilter(OpenGL::Nearest);
texture.setMagFilter(OpenGL::Nearest);
glBindTexture(GL_TEXTURE_2D, prevTexture);
fbo.createWithDrawTexture(texture, fmt == GL_DEPTH_STENCIL ? GL_DEPTH_STENCIL_ATTACHMENT : GL_DEPTH_ATTACHMENT);
glBindTexture(GL_TEXTURE_2D, prevTexture);
}
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
Helpers::panic("Incomplete framebuffer");
}
}
void free() {
void free() {
valid = false;
if (texture.exists()) {
texture.free();
}
}
bool matches(DepthBuffer& other) {
return location == other.location && format == other.format &&
size.x() == other.size.x() && size.y() == other.size.y();
}
bool matches(DepthBuffer& other) {
return location == other.location && format == other.format && size.x() == other.size.x() && size.y() == other.size.y();
}
size_t sizeInBytes() {
return (size_t)size.x() * (size_t)size.y() * PICA::sizePerPixel(format);
}
size_t sizeInBytes() {
return (size_t)size.x() * (size_t)size.y() * PICA::sizePerPixel(format);
}
};

View file

@ -4,6 +4,7 @@
#include "PICA/regs.hpp"
#include "boost/icl/interval.hpp"
#include "helpers.hpp"
#include "math_util.hpp"
#include "opengl.hpp"
template <typename T>
@ -40,11 +41,11 @@ struct Texture {
void allocate();
void setNewConfig(u32 newConfig);
void decodeTexture(const void* data);
void decodeTexture(std::span<const u8> data);
void free();
u64 sizeInBytes();
u32 decodeTexel(u32 u, u32 v, PICA::TextureFmt fmt, const void* data);
u32 decodeTexel(u32 u, u32 v, PICA::TextureFmt fmt, std::span<const u8> data);
// Get the morton interleave offset of a texel based on its U and V values
static u32 mortonInterleave(u32 u, u32 v);
@ -53,12 +54,12 @@ struct Texture {
static u32 getSwizzledOffset_4bpp(u32 u, u32 v, u32 width);
// Returns the format of this texture as a string
std::string formatToString() {
std::string_view formatToString() {
return PICA::textureFormatToString(format);
}
// Returns the texel at coordinates (u, v) of an ETC1(A4) texture
// TODO: Make hasAlpha a template parameter
u32 getTexelETC(bool hasAlpha, u32 u, u32 v, u32 width, const void* data);
u32 getTexelETC(bool hasAlpha, u32 u, u32 v, u32 width, std::span<const u8> data);
u32 decodeETC(u32 alpha, u32 u, u32 v, u64 colourData);
};

View file

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

View file

@ -0,0 +1,18 @@
#include "renderer.hpp"
class GPU;
class RendererSw final : public Renderer {
public:
RendererSw(GPU& gpu, const std::array<u32, regNum>& internalRegs, const std::array<u32, extRegNum>& externalRegs);
~RendererSw() override;
void reset() override;
void display() override;
void initGraphicsContext(SDL_Window* window) override;
void clearBuffer(u32 startAddress, u32 endAddress, u32 value, u32 control) override;
void displayTransfer(u32 inputAddr, u32 outputAddr, u32 inputSize, u32 outputSize, u32 flags) override;
void textureCopy(u32 inputAddr, u32 outputAddr, u32 totalBytes, u32 inputSize, u32 outputSize, u32 flags) override;
void drawVertices(PICA::PrimType primType, std::span<const PICA::Vertex> vertices) override;
void screenshot(const std::string& name) override;
};

View file

@ -0,0 +1,58 @@
#include "renderer.hpp"
#include "vulkan_api.hpp"
class GPU;
class RendererVK final : public Renderer {
SDL_Window* targetWindow;
// The order of these `Unique*` members is important, they will be destroyed in RAII order
vk::UniqueInstance instance = {};
vk::UniqueDebugUtilsMessengerEXT debugMessenger = {};
vk::UniqueSurfaceKHR surface = {};
vk::PhysicalDevice physicalDevice = {};
vk::UniqueDevice device = {};
vk::Queue presentQueue = {};
u32 presentQueueFamily = ~0u;
vk::Queue graphicsQueue = {};
u32 graphicsQueueFamily = ~0u;
vk::Queue computeQueue = {};
u32 computeQueueFamily = ~0u;
vk::Queue transferQueue = {};
u32 transferQueueFamily = ~0u;
vk::UniqueCommandPool commandPool = {};
vk::UniqueSwapchainKHR swapchain = {};
u32 swapchainImageCount = ~0u;
std::vector<vk::Image> swapchainImages = {};
std::vector<vk::UniqueImageView> swapchainImageViews = {};
// Per-swapchain-image data
// Each vector is `swapchainImageCount` in size
std::vector<vk::UniqueCommandBuffer> presentCommandBuffers = {};
std::vector<vk::UniqueSemaphore> swapImageFreeSemaphore = {};
std::vector<vk::UniqueSemaphore> renderFinishedSemaphore = {};
std::vector<vk::UniqueFence> frameFinishedFences = {};
// Recreate the swapchain, possibly re-using the old one in the case of a resize
vk::Result recreateSwapchain(vk::SurfaceKHR surface, vk::Extent2D swapchainExtent);
u64 currentFrame = 0;
public:
RendererVK(GPU& gpu, const std::array<u32, regNum>& internalRegs, const std::array<u32, extRegNum>& externalRegs);
~RendererVK() override;
void reset() override;
void display() override;
void initGraphicsContext(SDL_Window* window) override;
void clearBuffer(u32 startAddress, u32 endAddress, u32 value, u32 control) override;
void displayTransfer(u32 inputAddr, u32 outputAddr, u32 inputSize, u32 outputSize, u32 flags) override;
void textureCopy(u32 inputAddr, u32 outputAddr, u32 totalBytes, u32 inputSize, u32 outputSize, u32 flags) override;
void drawVertices(PICA::PrimType primType, std::span<const PICA::Vertex> vertices) override;
void screenshot(const std::string& name) override;
};

View file

@ -0,0 +1,48 @@
#pragma once
#include <span>
#include <type_traits>
#include <utility>
#include "vulkan_api.hpp"
namespace Vulkan {
VKAPI_ATTR VkBool32 VKAPI_CALL debugMessageCallback(
VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, VkDebugUtilsMessageTypeFlagsEXT messageType,
const VkDebugUtilsMessengerCallbackDataEXT* callbackData, void* userData
);
void setObjectName(vk::Device device, vk::ObjectType objectType, const void* objectHandle, const char* format, ...);
template <typename T, typename = std::enable_if_t<vk::isVulkanHandleType<T>::value == true>, typename... ArgsT>
inline void setObjectName(vk::Device device, const T objectHandle, const char* format, ArgsT&&... args) {
setObjectName(device, T::objectType, objectHandle, format, std::forward<ArgsT>(args)...);
}
void beginDebugLabel(vk::CommandBuffer commandBuffer, std::span<const float, 4> color, const char* format, ...);
void insertDebugLabel(vk::CommandBuffer commandBuffer, std::span<const float, 4> color, const char* format, ...);
void endDebugLabel(vk::CommandBuffer commandBuffer);
class DebugLabelScope {
private:
const vk::CommandBuffer commandBuffer;
public:
template <typename... ArgsT>
DebugLabelScope(vk::CommandBuffer targetCommandBuffer, std::span<const float, 4> color, const char* format, ArgsT&&... args)
: commandBuffer(targetCommandBuffer) {
beginDebugLabel(commandBuffer, color, format, std::forward<ArgsT>(args)...);
}
template <typename... ArgsT>
void operator()(std::span<const float, 4> color, const char* format, ArgsT&&... args) const {
insertDebugLabel(commandBuffer, color, format, std::forward<ArgsT>(args)...);
}
~DebugLabelScope() { endDebugLabel(commandBuffer); }
};
} // namespace Vulkan

View file

@ -0,0 +1,12 @@
#pragma once
#define VK_NO_PROTOTYPES
#include <vulkan/vulkan.h>
#define VULKAN_HPP_DISPATCH_LOADER_DYNAMIC 1
#define VULKAN_HPP_NO_EXCEPTIONS
// Disable asserts on result-codes
#define VULKAN_HPP_ASSERT_ON_RESULT
#include <vulkan/vulkan.hpp>
#include <vulkan/vulkan_format_traits.hpp>
#include <vulkan/vulkan_hash.hpp>

View file

@ -1,5 +1,6 @@
#pragma once
#include "result_cfg.hpp"
#include "result_common.hpp"
#include "result_kernel.hpp"
#include "result_os.hpp"

View file

@ -0,0 +1,8 @@
#pragma once
#include "result_common.hpp"
DEFINE_HORIZON_RESULT_MODULE(Result::CFG, Config);
namespace Result::CFG {
DEFINE_HORIZON_RESULT(NotFound, 1018, WrongArgument, Permanent);
};

View file

@ -11,6 +11,11 @@ class ACService {
MAKE_LOG_FUNCTION(log, acLogger)
// Service commands
void cancelConnectAsync(u32 messagePointer);
void closeAsync(u32 messagePointer);
void createDefaultConfig(u32 messagePointer);
void getLastErrorCode(u32 messagePointer);
void registerDisconnectEvent(u32 messagePointer);
void setClientVersion(u32 messagePointer);
public:

View file

@ -13,6 +13,7 @@ class ACTService {
// Service commands
void initialize(u32 messagePointer);
void generateUUID(u32 messagePointer);
void getAccountDataBlock(u32 messagePointer);
public:
ACTService(Memory& mem) : mem(mem) {}

View file

@ -12,6 +12,7 @@ class AMService {
// Service commands
void getDLCTitleInfo(u32 messagePointer);
void getPatchTitleInfo(u32 messagePointer);
void listTitleInfo(u32 messagePointer);
public:

View file

@ -11,15 +11,24 @@ class BOSSService {
MAKE_LOG_FUNCTION(log, bossLogger)
// Service commands
void cancelTask(u32 messagePointer);
void initializeSession(u32 messagePointer);
void getNsDataIdList(u32 messagePointer);
void getErrorCode(u32 messagePointer);
void getNsDataIdList(u32 messagePointer, u32 commandWord);
void getOptoutFlag(u32 messagePointer);
void getStorageEntryInfo(u32 messagePointer); // Unknown what this is, name taken from Citra
void getTaskIdList(u32 messagePointer);
void getTaskInfo(u32 messagePOinter);
void getTaskInfo(u32 messagePointer);
void getTaskServiceStatus(u32 messagePointer);
void getTaskState(u32 messagePointer);
void getTaskStatus(u32 messagePointer);
void getTaskStorageInfo(u32 messagePointer);
void receiveProperty(u32 messagePointer);
void registerNewArrivalEvent(u32 messagePointer);
void registerStorageEntry(u32 messagePointer);
void registerTask(u32 messagePointer);
void sendProperty(u32 messagePointer);
void startTask(u32 messagePointer);
void unregisterStorage(u32 messagePointer);
void unregisterTask(u32 messagePointer);

View file

@ -1,21 +1,33 @@
#pragma once
#include <array>
#include <optional>
#include "helpers.hpp"
#include "kernel_types.hpp"
#include "logger.hpp"
#include "memory.hpp"
#include "result/result.hpp"
// Yay, circular dependencies!
class Kernel;
class CAMService {
Handle handle = KernelHandles::CAM;
Memory& mem;
Kernel& kernel;
MAKE_LOG_FUNCTION(log, camLogger)
using Event = std::optional<Handle>;
static constexpr size_t portCount = 4; // PORT_NONE, PORT_CAM1, PORT_CAM2, PORT_BOTH
std::array<Event, portCount> bufferErrorInterruptEvents;
// Service commands
void driverInitialize(u32 messagePointer);
void getMaxLines(u32 messagePointer);
void getBufferErrorInterruptEvent(u32 messagePointer);
public:
CAMService(Memory& mem) : mem(mem) {}
public:
CAMService(Memory& mem, Kernel& kernel) : mem(mem), kernel(kernel) {}
void reset();
void handleSyncRequest(u32 messagePointer);
};

View file

@ -18,6 +18,7 @@ class CECDService {
// Service commands
void getInfoEventHandle(u32 messagePointer);
void openAndRead(u32 messagePointer);
public:
CECDService(Memory& mem, Kernel& kernel) : mem(mem), kernel(kernel) {}

View file

@ -7,7 +7,6 @@
#include "result/result.hpp"
class CFGService {
Handle handle = KernelHandles::CFG;
Memory& mem;
CountryCodes country = CountryCodes::US; // Default to USA
MAKE_LOG_FUNCTION(log, cfgLogger)
@ -16,6 +15,7 @@ class CFGService {
// Service functions
void getConfigInfoBlk2(u32 messagePointer);
void getCountryCodeID(u32 messagePointer);
void getRegionCanadaUSA(u32 messagePointer);
void getSystemModel(u32 messagePointer);
void genUniqueConsoleHash(u32 messagePointer);

View file

@ -19,6 +19,8 @@ class FRDService {
Memory& mem;
MAKE_LOG_FUNCTION(log, frdLogger)
bool loggedIn = false;
// Service commands
void attachToEventNotification(u32 messagePointer);
void getFriendKeyList(u32 messagePointer);
@ -27,8 +29,11 @@ class FRDService {
void getMyPresence(u32 messagePointer);
void getMyProfile(u32 messagePointer);
void getMyScreenName(u32 messsagePointer);
void hasLoggedIn(u32 messagePointer);
void logout(u32 messagePointer);
void setClientSDKVersion(u32 messagePointer);
void setNotificationMask(u32 messagePointer);
void updateGameModeDescription(u32 messagePointer);
public:
FRDService(Memory& mem) : mem(mem) {}

View file

@ -37,9 +37,11 @@ class FSService {
// Service commands
void createDirectory(u32 messagePointer);
void createExtSaveData(u32 messagePointer);
void createFile(u32 messagePointer);
void closeArchive(u32 messagePointer);
void controlArchive(u32 messagePointer);
void deleteExtSaveData(u32 messagePointer);
void deleteFile(u32 messagePointer);
void formatSaveData(u32 messagePointer);
void formatThisUserSaveData(u32 messagePointer);

View file

@ -40,11 +40,32 @@ class GPUService {
MAKE_LOG_FUNCTION(log, gspGPULogger)
void processCommandBuffer();
struct FramebufferInfo {
u32 activeFb;
u32 leftFramebufferVaddr;
u32 rightFramebufferVaddr;
u32 stride;
u32 format;
u32 displayFb;
u32 attribute;
};
static_assert(sizeof(FramebufferInfo) == 28, "GSP::GPU::FramebufferInfo has the wrong size");
struct FramebufferUpdate {
u8 index;
u8 dirtyFlag;
u16 pad0;
std::array<FramebufferInfo, 2> framebufferInfo;
u32 pad1;
};
static_assert(sizeof(FramebufferUpdate) == 64, "GSP::GPU::FramebufferUpdate has the wrong size");
// Service commands
void acquireRight(u32 messagePointer);
void flushDataCache(u32 messagePointer);
void registerInterruptRelayQueue(u32 messagePointer);
void setAxiConfigQoSMode(u32 messagePointer);
void setBufferSwap(u32 messagePointer);
void setInternalPriorities(u32 messagePointer);
void setLCDForceBlack(u32 messagePointer);
void storeDataCache(u32 messagePointer);
@ -60,6 +81,8 @@ class GPUService {
void triggerTextureCopy(u32* cmd);
void flushCacheRegions(u32* cmd);
void setBufferSwapImpl(u32 screen_id, const FramebufferInfo& info);
public:
GPUService(Memory& mem, GPU& gpu, Kernel& kernel, u32& currentPID) : mem(mem), gpu(gpu),
kernel(kernel), currentPID(currentPID) {}

View file

@ -71,6 +71,7 @@ class HIDService {
void getGyroscopeLowCalibrateParam(u32 messagePointer);
void getGyroscopeCoefficient(u32 messagePointer);
void getIPCHandles(u32 messagePointer);
void getSoundVolume(u32 messagePointer);
// Don't call these prior to initializing shared mem pls
template <typename T>
@ -91,8 +92,9 @@ class HIDService {
void pressKey(u32 mask) { newButtons |= mask; }
void releaseKey(u32 mask) { newButtons &= ~mask; }
s16 getCirclepadX() { return circlePadX; }
s16 getCirclepadY() { return circlePadY; }
u32 getOldButtons() const { return oldButtons; }
s16 getCirclepadX() const { return circlePadX; }
s16 getCirclepadY() const { return circlePadY; }
void setCirclepadX(s16 x) {
circlePadX = x;
@ -140,4 +142,6 @@ class HIDService {
void releaseTouchScreen() {
touchScreenPressed = false;
}
bool isTouchScreenPressed() { return touchScreenPressed; }
};

21
include/services/http.hpp Normal file
View file

@ -0,0 +1,21 @@
#pragma once
#include "helpers.hpp"
#include "kernel_types.hpp"
#include "logger.hpp"
#include "memory.hpp"
class HTTPService {
Handle handle = KernelHandles::HTTP;
Memory& mem;
MAKE_LOG_FUNCTION(log, httpLogger)
bool initialized = false;
// Service commands
void initialize(u32 messagePointer);
public:
HTTPService(Memory& mem) : mem(mem) {}
void reset();
void handleSyncRequest(u32 messagePointer);
};

View file

@ -11,6 +11,10 @@
class Kernel;
class IRUserService {
enum class DeviceID : u8 {
CirclePadPro = 1,
};
Handle handle = KernelHandles::IR_USER;
Memory& mem;
Kernel& kernel;
@ -20,10 +24,35 @@ class IRUserService {
void disconnect(u32 messagePointer);
void finalizeIrnop(u32 messagePointer);
void getConnectionStatusEvent(u32 messagePointer);
void getReceiveEvent(u32 messagePointer);
void initializeIrnopShared(u32 messagePointer);
void requireConnection(u32 messagePointer);
void sendIrnop(u32 messagePointer);
std::optional<Handle> connectionStatusEvent = std::nullopt;
using IREvent = std::optional<Handle>;
IREvent connectionStatusEvent = std::nullopt;
IREvent receiveEvent = std::nullopt;
std::optional<MemoryBlock> sharedMemory = std::nullopt;
bool connectedDevice = false;
// Header of the IR shared memory containing various bits of info
// https://www.3dbrew.org/wiki/IRUSER_Shared_Memory
struct SharedMemoryStatus {
u32 latestReceiveError;
u32 latestSharedError;
u8 connectionStatus;
u8 connectionAttemptStatus;
u8 connectionRole;
u8 machineID;
u8 isConnected;
u8 networkID;
u8 isInitialized; // https://github.com/citra-emu/citra/blob/c10ffda91feb3476a861c47fb38641c1007b9d33/src/core/hle/service/ir/ir_user.cpp#L41
u8 unk1;
};
static_assert(sizeof(SharedMemoryStatus) == 16);
public:
IRUserService(Memory& mem, Kernel& kernel) : mem(mem), kernel(kernel) {}

View file

@ -0,0 +1,24 @@
#pragma once
#include "config.hpp"
#include "helpers.hpp"
#include "kernel_types.hpp"
#include "logger.hpp"
#include "memory.hpp"
namespace MCU {
class HWCService {
Handle handle = KernelHandles::MCU_HWC;
Memory& mem;
MAKE_LOG_FUNCTION(log, mcuLogger)
const EmulatorConfig& config;
// Service commands
void getBatteryLevel(u32 messagePointer);
public:
HWCService(Memory& mem, const EmulatorConfig& config) : mem(mem), config(config) {}
void reset();
void handleSyncRequest(u32 messagePointer);
};
} // namespace MCU

View file

@ -15,13 +15,16 @@ class MICService {
void mapSharedMem(u32 messagePointer);
void setClamp(u32 messagePointer);
void setGain(u32 messagePointer);
void setIirFilter(u32 messagePointer);
void setPower(u32 messagePointer);
void startSampling(u32 messagePointer);
void stopSampling(u32 messagePointer);
void theCaptainToadFunction(u32 messagePointer);
u8 gain = 0; // How loud our microphone input signal is
bool micEnabled = false;
bool shouldClamp = false;
bool isSampling = false;
public:
MICService(Memory& mem) : mem(mem) {}

View file

@ -11,6 +11,7 @@ class NDMService {
MAKE_LOG_FUNCTION(log, ndmLogger)
// Service commands
void clearHalfAwakeMacFilter(u32 messagePointer);
void overrideDefaultDaemons(u32 messagePointer);
void resumeDaemons(u32 messagePointer);
void resumeScheduler(u32 messagePointer);

View file

@ -0,0 +1,18 @@
#pragma once
#include "helpers.hpp"
#include "kernel_types.hpp"
#include "logger.hpp"
#include "memory.hpp"
class NewsUService {
Handle handle = KernelHandles::NEWS_U;
Memory& mem;
MAKE_LOG_FUNCTION(log, newsLogger)
// Service commands
public:
NewsUService(Memory& mem) : mem(mem) {}
void reset();
void handleSyncRequest(u32 messagePointer);
};

View file

@ -14,13 +14,38 @@ class NFCService {
Kernel& kernel;
MAKE_LOG_FUNCTION(log, nfcLogger)
enum class Old3DSAdapterStatus : u32 {
Idle = 0,
AttemptingToInitialize = 1,
InitializationComplete = 2,
Active = 3,
};
enum class TagStatus : u8 {
NotInitialized = 0,
Initialized = 1,
Scanning = 2,
InRange = 3,
OutOfRange = 4,
Loaded = 5,
};
// Kernel events signaled when an NFC tag goes in and out of range respectively
std::optional<Handle> tagInRangeEvent, tagOutOfRangeEvent;
Old3DSAdapterStatus adapterStatus;
TagStatus tagStatus;
bool initialized = false;
// Service commands
void communicationGetResult(u32 messagePointer);
void communicationGetStatus(u32 messagePointer);
void initialize(u32 messagePointer);
void getTagInRangeEvent(u32 messagePointer);
void getTagOutOfRangeEvent(u32 messagePointer);
void getTagState(u32 messagePointer);
void startCommunication(u32 messagePointer);
void stopCommunication(u32 messagePointer);
public:
NFCService(Memory& mem, Kernel& kernel) : mem(mem), kernel(kernel) {}

View file

@ -1,4 +1,5 @@
#pragma once
#include "config.hpp"
#include "helpers.hpp"
#include "kernel_types.hpp"
#include "logger.hpp"
@ -10,13 +11,39 @@ class PTMService {
Memory& mem;
MAKE_LOG_FUNCTION(log, ptmLogger)
const EmulatorConfig& config;
// Service commands
void configureNew3DSCPU(u32 messagePointer);
void getAdapterState(u32 messagePointer);
void getBatteryLevel(u32 messagePointer);
void getStepHistory(u32 messagePointer);
void getTotalStepCount(u32 messagePointer);
public:
PTMService(Memory& mem) : mem(mem) {}
PTMService(Memory& mem, const EmulatorConfig& config) : mem(mem), config(config) {}
void reset();
void handleSyncRequest(u32 messagePointer);
// 0% -> 0 (shutting down)
// 1-5% -> 1
// 6-10% -> 2
// 11-30% -> 3
// 31-60% -> 4
// 61-100% -> 5
static constexpr u8 batteryPercentToLevel(u8 percent) {
if (percent == 0) {
return 0;
} else if (percent >= 1 && percent <= 5) {
return 1;
} else if (percent >= 6 && percent <= 10) {
return 2;
} else if (percent >= 11 && percent <= 30) {
return 3;
} else if (percent >= 31 && percent <= 60) {
return 4;
} else {
return 5;
}
}
};

View file

@ -21,15 +21,21 @@
#include "services/gsp_gpu.hpp"
#include "services/gsp_lcd.hpp"
#include "services/hid.hpp"
#include "services/http.hpp"
#include "services/ir_user.hpp"
#include "services/ldr_ro.hpp"
#include "services/mcu/mcu_hwc.hpp"
#include "services/mic.hpp"
#include "services/ndm.hpp"
#include "services/news_u.hpp"
#include "services/nfc.hpp"
#include "services/nim.hpp"
#include "services/ptm.hpp"
#include "services/soc.hpp"
#include "services/ssl.hpp"
#include "services/y2r.hpp"
struct EmulatorConfig;
// More circular dependencies!!
class Kernel;
@ -53,6 +59,7 @@ class ServiceManager {
DlpSrvrService dlp_srvr;
DSPService dsp;
HIDService hid;
HTTPService http;
IRUserService ir_user;
FRDService frd;
FSService fs;
@ -60,12 +67,17 @@ class ServiceManager {
LCDService gsp_lcd;
LDRService ldr;
MICService mic;
NDMService ndm;
NewsUService news_u;
NFCService nfc;
NIMService nim;
NDMService ndm;
PTMService ptm;
SOCService soc;
SSLService ssl;
Y2RService y2r;
MCU::HWCService mcu_hwc;
// "srv:" commands
void enableNotification(u32 messagePointer);
void getServiceHandle(u32 messagePointer);
@ -74,7 +86,7 @@ class ServiceManager {
void subscribe(u32 messagePointer);
public:
ServiceManager(std::span<u32, 16> regs, Memory& mem, GPU& gpu, u32& currentPID, Kernel& kernel);
ServiceManager(std::span<u32, 16> regs, Memory& mem, GPU& gpu, u32& currentPID, Kernel& kernel, const EmulatorConfig& config);
void reset();
void initializeFS() { fs.initializeFilesystem(); }
void handleSyncRequest(u32 messagePointer);
@ -90,17 +102,5 @@ class ServiceManager {
void signalDSPEvents() { dsp.signalEvents(); }
// Input function wrappers
void pressKey(u32 key) { hid.pressKey(key); }
void releaseKey(u32 key) { hid.releaseKey(key); }
s16 getCirclepadX() { return hid.getCirclepadX(); }
s16 getCirclepadY() { return hid.getCirclepadY(); }
void setCirclepadX(s16 x) { hid.setCirclepadX(x); }
void setCirclepadY(s16 y) { hid.setCirclepadY(y); }
void updateInputs(u64 currentTimestamp) { hid.updateInputs(currentTimestamp); }
void setTouchScreenPress(u16 x, u16 y) { hid.setTouchScreenPress(x, y); }
void releaseTouchScreen() { hid.releaseTouchScreen(); }
void setRoll(s16 roll) { hid.setRoll(roll); }
void setPitch(s16 pitch) { hid.setPitch(pitch); }
void setYaw(s16 yaw) { hid.setYaw(yaw); }
HIDService& getHID() { return hid; }
};

View file

@ -1,5 +0,0 @@
#pragma once
#include <cstddef>
extern unsigned char _shared_font_bin[];
extern size_t _shared_font_len;

21
include/services/soc.hpp Normal file
View file

@ -0,0 +1,21 @@
#pragma once
#include "helpers.hpp"
#include "kernel_types.hpp"
#include "logger.hpp"
#include "memory.hpp"
class SOCService {
Handle handle = KernelHandles::SOC;
Memory& mem;
MAKE_LOG_FUNCTION(log, socLogger)
bool initialized = false;
// Service commands
void initializeSockets(u32 messagePointer);
public:
SOCService(Memory& mem) : mem(mem) {}
void reset();
void handleSyncRequest(u32 messagePointer);
};

25
include/services/ssl.hpp Normal file
View file

@ -0,0 +1,25 @@
#pragma once
#include "helpers.hpp"
#include "kernel_types.hpp"
#include "logger.hpp"
#include "memory.hpp"
#include <random>
class SSLService {
Handle handle = KernelHandles::SSL;
Memory& mem;
MAKE_LOG_FUNCTION(log, sslLogger)
std::mt19937 rng; // Use a Mersenne Twister for RNG since this service is supposed to have better rng than just rand()
bool initialized;
// Service commands
void initialize(u32 messagePointer);
void generateRandomData(u32 messagePointer);
public:
SSLService(Memory& mem) : mem(mem) {}
void reset();
void handleSyncRequest(u32 messagePointer);
};

View file

@ -75,6 +75,7 @@ class Y2RService {
void setInputLineWidth(u32 messagePointer);
void setInputLines(u32 messagePointer);
void setOutputFormat(u32 messagePointer);
void setPackageParameter(u32 messagePointer);
void setReceiving(u32 messagePointer);
void setRotation(u32 messagePointer);
void setSendingY(u32 messagePointer);

View file

@ -8,17 +8,27 @@ Join our Discord server by pressing on the banner below!
[![Discord Banner 2](https://discord.com/api/guilds/1118695732958994532/widget.png?style=banner2)](https://discord.gg/ZYbugsEmsw)
![screenshot1](docs/img/MK7.png) ![screenshot2](docs/img/OoT_Title.png) ![screenshot3](docs/img/pokegang.png)
![screenshot1](docs/img/KirbyRobobot.png) ![screenshot2](docs/img/OoT_Title.png) ![screenshot3](docs/img/pokegang.png)
# Download
You can download stable builds from the Releases tab, or you can download the latest build from the table below
|Platform|Status|Download|
|--------|------------|--------|
|Windows build|[![Windows Build](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/Windows_Build.yml/badge.svg?branch=master)](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/Windows_Build.yml)|[Windows Executable](https://nightly.link/wheremyfoodat/Panda3DS/workflows/Windows_Build/master/Windows%20executable.zip)|
|MacOS build|[![MacOS Build](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/MacOS_Build.yml/badge.svg?branch=master)](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/MacOS_Build.yml)|[MacOS App Bundle](https://nightly.link/wheremyfoodat/Panda3DS/workflows/MacOS_Build/master/MacOS%20Alber%20App%20Bundle.zip)|
|Linux build|[![Linux Build](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/Linux_Build.yml/badge.svg?branch=master)](https://github.com/wheremyfoodat/Panda3DS/actions/workflows/Linux_Build.yml)|[Linux Executable](https://nightly.link/wheremyfoodat/Panda3DS/workflows/Linux_Build/master/Linux%20executable.zip)|
# Compatibility
Panda3DS is still in the early stages of development. Many games boot, many don't. Most games have at least some hilariously broken graphics, audio is not supported, performance leaves a bit to be desired mainly thanks to lack of shader acceleration, and most QoL features (including a GUI) are missing.
Panda3DS is still in the early stages of development. Many games boot, many don't. Most games have at least some hilariously broken graphics, audio is not supported, and some QoL features (including a GUI) are missing.
In addition, some games don't quiiite work with the upstream code. A lot of them might need some panics in the source code to be commented out before they work, etc. However, just the fact things can work as well as they do now is promising in itself.
Check out [this Google spreadsheet](https://docs.google.com/spreadsheets/d/1nWZTzfaMPkZdyhqHEawMRBaP0qSMmQdxrVfAbgapYrM/edit?usp=sharing) for an unofficial compatibility list.
# Why?
The 3DS emulation scene is already pretty mature, with offerings such as [Citra](https://github.com/citra-emu/citra) which can offer a great playing experience for most games in the library, [Corgi3DS](https://github.com/PSI-Rockin/Corgi3DS), an innovative LLE emulator, or [Mikage](https://mikage.app/). However, there's always room for more emulators! While Panda3DS was initially a mere curiosity, there's many different concepts I would like to explore with it in the future, such as:
- Virtualization. What motivated the creation of this emulator was actually a discussion on whether it is possible to get fast 3DS emulation on low-end hardware such as the Raspberry Pi 4, using the KVM API. At the moment, Panda3DS is powered by dynarmic rather than using virtualization, but this is definitely a concept I want to explore in the future.
- Virtualization. What motivated the creation of this emulator was actually a discussion on whether it is possible to get fast 3DS emulation on low-end hardware such as the Raspberry Pi 4, using the KVM API. At the moment, Panda3DS is powered by Dynarmic rather than using virtualization, but this is definitely a concept I want to explore in the future.
- Debugging, reverse engineering and modding tools. While contributing to [PCSX-Redux](https://github.com/grumpycoders/pcsx-redux) and collaborating with the other developers, I had the chance to find out how useful tools like these can be. They can serve as indispensable tools for the homebrew devs, modders, reverse engineers, as well as emulator developers themselves. Some tools can even become fun toys the casual user can mess around with. As such, I think they can really improve the experience in a project like this. Of course, I'd like to thank @nicolasnoble and the entire Redux team for helping me learn the value of these tools, as well as making me improve as a programmer.
@ -29,7 +39,7 @@ The 3DS emulation scene is already pretty mature, with offerings such as [Citra]
Keep in mind, these are all long-term plans. Until then, the main focus is just improving compatibility
# How to build
Panda3DS compiles on Windows, Linux and MacOS, without needing to download any system dependencies.
Panda3DS compiles on Windows, Linux and MacOS, with only 1 system dependency, the Vulkan SDK. However, if you don't want to install the Vulkan SDK you can always build the emulator with only OpenGL support, by adding `-DENABLE_VULKAN=OFF` to the `cmake` command
All you need is CMake and a generator of your choice (Make, Visual Studio, Ninja, etc). Simply clone the repo recursively and build it like your average CMake project.
@ -70,6 +80,9 @@ Keyboard & Mouse
- Select button Backspace
- Touch Screen Left click
- Gyroscope Hold right click and swipe your mouse left and right (support is kind of shaky atm, but games that require gyro here and there like Kirby should work)
- Pause/Resume F4
- Reload F5
Panda3DS also supports controller input using the SDL2 GameController API.
@ -91,6 +104,8 @@ Panda3DS also supports controller input using the SDL2 GameController API.
- [MelonDS](https://github.com/melonDS-emu/melonDS): "DS emulator, sorta" - Arisotura
- [Kaizen](https://github.com/SimoneN64/Kaizen): Experimental work-in-progress low-level N64 emulator
- [ChonkyStation](https://github.com/liuk7071/ChonkyStation): Work-in-progress PlayStation emulator
- [shadPS4](https://github.com/georgemoralis/shadPS4): Work-in-progress PS4 emulator by the founder of PCSX, PCSX2 and more
- [Hydra](https://github.com/hydra-emu/hydra): Cross-platform GameBoy, NES, N64 and Chip-8 emulator
# Support
If you find this project exciting and want to support the founder, check out [his Patreon page](https://www.patreon.com/wheremyfoodat)
@ -99,5 +114,5 @@ Keep in mind, funding is only aimed to cover various life costs and support deve
Nintendo 3DS is a registered trademark of Nintendo Co., Ltd.
![pamda](docs/img/panda.jpg)
![panda](docs/img/panda.jpg)
Here's a panda it go blep

View file

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

View file

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

View file

@ -143,6 +143,9 @@ void ShaderEmitter::compileInstruction(const PICAShader& shaderUnit) {
break;
case ShaderOpcodes::DP3: recDP3(shaderUnit, instruction); break;
case ShaderOpcodes::DP4: recDP4(shaderUnit, instruction); break;
case ShaderOpcodes::DPH:
case ShaderOpcodes::DPHI:
recDPH(shaderUnit, instruction); break;
case ShaderOpcodes::END: recEND(shaderUnit, instruction); break;
case ShaderOpcodes::EX2: recEX2(shaderUnit, instruction); break;
case ShaderOpcodes::FLR: recFLR(shaderUnit, instruction); break;
@ -179,6 +182,10 @@ void ShaderEmitter::compileInstruction(const PICAShader& shaderUnit) {
case ShaderOpcodes::SLTI:
recSLT(shaderUnit, instruction); break;
case ShaderOpcodes::SGE:
case ShaderOpcodes::SGEI:
recSGE(shaderUnit, instruction); break;
default:
Helpers::panic("Shader JIT: Unimplemented PICA opcode %X", opcode);
}
@ -525,6 +532,32 @@ void ShaderEmitter::recDP4(const PICAShader& shader, u32 instruction) {
storeRegister(src1_xmm, shader, dest, operandDescriptor);
}
void ShaderEmitter::recDPH(const PICAShader& shader, u32 instruction) {
const bool isDPHI = (instruction >> 26) == ShaderOpcodes::DPHI;
const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f];
const u32 src1 = isDPHI ? getBits<14, 5>(instruction) : getBits<12, 7>(instruction);
const u32 src2 = isDPHI ? getBits<7, 7>(instruction) : getBits<7, 5>(instruction);
const u32 idx = getBits<19, 2>(instruction);
const u32 dest = getBits<21, 5>(instruction);
// TODO: Safe multiplication equivalent (Multiplication is not IEEE compliant on the PICA)
loadRegister<1>(src1_xmm, shader, src1, isDPHI ? 0 : idx, operandDescriptor);
loadRegister<2>(src2_xmm, shader, src2, isDPHI ? idx : 0, operandDescriptor);
// Attach 1.0 to the w component of src1
if (haveSSE4_1) {
blendps(src1_xmm, xword[rip + onesVector], 0b1000);
} else {
movaps(scratch1, src1_xmm);
unpckhps(scratch1, xword[rip + onesVector]);
unpcklpd(src1_xmm, scratch1);
}
dpps(src1_xmm, src2_xmm, 0b11111111); // 4-lane dot product between the 2 registers, store the result in all lanes of scratch1 similarly to PICA
storeRegister(src1_xmm, shader, dest, operandDescriptor);
}
void ShaderEmitter::recMAX(const PICAShader& shader, u32 instruction) {
const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f];
const u32 src1 = getBits<12, 7>(instruction);
@ -656,6 +689,24 @@ void ShaderEmitter::recSLT(const PICAShader& shader, u32 instruction) {
storeRegister(src1_xmm, shader, dest, operandDescriptor);
}
void ShaderEmitter::recSGE(const PICAShader& shader, u32 instruction) {
const bool isSGEI = (instruction >> 26) == ShaderOpcodes::SGEI;
const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f];
const u32 src1 = isSGEI ? getBits<14, 5>(instruction) : getBits<12, 7>(instruction);
const u32 src2 = isSGEI ? getBits<7, 7>(instruction) : getBits<7, 5>(instruction);
const u32 idx = getBits<19, 2>(instruction);
const u32 dest = getBits<21, 5>(instruction);
loadRegister<1>(src1_xmm, shader, src1, isSGEI ? 0 : idx, operandDescriptor);
loadRegister<2>(src2_xmm, shader, src2, isSGEI ? idx : 0, operandDescriptor);
// SSE does not have a cmpgeps instruction so we turn src1 >= src2 to src2 <= src1, result in src2
cmpleps(src2_xmm, src1_xmm);
andps(src2_xmm, xword[rip + onesVector]);
storeRegister(src2_xmm, shader, dest, operandDescriptor);
}
void ShaderEmitter::recCMP(const PICAShader& shader, u32 instruction) {
const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f];
const u32 src1 = getBits<12, 7>(instruction);

View file

@ -7,10 +7,20 @@
#include "PICA/float_types.hpp"
#include "PICA/regs.hpp"
#include "renderer_null/renderer_null.hpp"
#include "renderer_sw/renderer_sw.hpp"
#ifdef PANDA3DS_ENABLE_OPENGL
#include "renderer_gl/renderer_gl.hpp"
#endif
#ifdef PANDA3DS_ENABLE_VULKAN
#include "renderer_vk/renderer_vk.hpp"
#endif
constexpr u32 topScreenWidth = 240;
constexpr u32 topScreenHeight = 400;
constexpr u32 bottomScreenWidth = 240;
constexpr u32 bottomScreenHeight = 300;
using namespace Floats;
@ -20,10 +30,34 @@ GPU::GPU(Memory& mem, EmulatorConfig& config) : mem(mem), config(config) {
vram = new u8[vramSize];
mem.setVRAM(vram); // Give the bus a pointer to our VRAM
// TODO: Configurable backend
switch (config.rendererType) {
case RendererType::Null: {
renderer.reset(new RendererNull(*this, regs, externalRegs));
break;
}
case RendererType::Software: {
renderer.reset(new RendererSw(*this, regs, externalRegs));
break;
}
#ifdef PANDA3DS_ENABLE_OPENGL
renderer.reset(new RendererGL(*this, regs));
case RendererType::OpenGL: {
renderer.reset(new RendererGL(*this, regs, externalRegs));
break;
}
#endif
#ifdef PANDA3DS_ENABLE_VULKAN
case RendererType::Vulkan: {
renderer.reset(new RendererVK(*this, regs, externalRegs));
break;
}
#endif
default: {
Helpers::panic("Rendering backend not supported: %s", Renderer::typeToString(config.rendererType));
break;
}
}
}
void GPU::reset() {
@ -50,6 +84,27 @@ void GPU::reset() {
e.config2 = 0;
}
// Initialize the framebuffer registers. Values taken from Citra.
using namespace PICA::ExternalRegs;
// Top screen addresses and dimentions.
externalRegs[Framebuffer0AFirstAddr] = 0x181E6000;
externalRegs[Framebuffer0ASecondAddr] = 0x1822C800;
externalRegs[Framebuffer0BFirstAddr] = 0x18273000;
externalRegs[Framebuffer0BSecondAddr] = 0x182B9800;
externalRegs[Framebuffer0Size] = (topScreenHeight << 16) | topScreenWidth;
externalRegs[Framebuffer0Stride] = 720;
externalRegs[Framebuffer0Config] = static_cast<u32>(PICA::ColorFmt::RGB8);
externalRegs[Framebuffer0Select] = 0;
// Bottom screen addresses and dimentions.
externalRegs[Framebuffer1AFirstAddr] = 0x1848F000;
externalRegs[Framebuffer1ASecondAddr] = 0x184C7800;
externalRegs[Framebuffer1Size] = (bottomScreenHeight << 16) | bottomScreenWidth;
externalRegs[Framebuffer1Stride] = 720;
externalRegs[Framebuffer1Config] = static_cast<u32>(PICA::ColorFmt::RGB8);
externalRegs[Framebuffer1Select] = 0;
renderer->reset();
}
@ -293,15 +348,17 @@ PICA::Vertex GPU::getImmediateModeVertex() {
// Run VS and return vertex data. TODO: Don't hardcode offsets for each attribute
shaderUnit.vs.run();
std::memcpy(&v.s.positions, &shaderUnit.vs.outputs[0], sizeof(vec4f));
std::memcpy(&v.s.colour, &shaderUnit.vs.outputs[1], sizeof(vec4f));
std::memcpy(&v.s.texcoord0, &shaderUnit.vs.outputs[2], 2 * sizeof(f24));
printf(
"(x, y, z, w) = (%f, %f, %f, %f)\n", (double)v.s.positions[0], (double)v.s.positions[1], (double)v.s.positions[2], (double)v.s.positions[3]
);
printf("(r, g, b, a) = (%f, %f, %f, %f)\n", (double)v.s.colour[0], (double)v.s.colour[1], (double)v.s.colour[2], (double)v.s.colour[3]);
printf("(u, v ) = (%f, %f)\n", (double)v.s.texcoord0[0], (double)v.s.texcoord0[1]);
// Map shader outputs to fixed function properties
const u32 totalShaderOutputs = regs[PICA::InternalRegs::ShaderOutputCount] & 7;
for (int i = 0; i < totalShaderOutputs; i++) {
const u32 config = regs[PICA::InternalRegs::ShaderOutmap0 + i];
for (int j = 0; j < 4; j++) { // pls unroll
const u32 mapping = (config >> (j * 8)) & 0x1F;
v.raw[mapping] = shaderUnit.vs.outputs[i][j];
}
}
return v;
}

View file

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

View file

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

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

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

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

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

View file

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

View file

@ -19,14 +19,17 @@ HorizonResult SaveDataArchive::createFile(const FSPath& path, u64 size) {
// If the size is 0, leave the file empty and return success
if (size == 0) {
file.close();
return Result::Success;
}
// If it is not empty, seek to size - 1 and write a 0 to create a file of size "size"
else if (file.seek(size - 1, SEEK_SET) && file.writeBytes("", 1).second == 1) {
file.close();
return Result::Success;
}
file.close();
return Result::FS::FileTooLarge;
}
@ -156,6 +159,8 @@ Rust::Result<ArchiveBase::FormatInfo, HorizonResult> SaveDataArchive::getFormatI
FormatInfo ret;
auto [success, bytesRead] = file.readBytes(&ret, sizeof(FormatInfo));
file.close();
if (!success || bytesRead != sizeof(FormatInfo)) {
Helpers::warn("SaveData::GetFormatInfo: Format file exists but was not properly read into the FormatInfo struct");
return Err(Result::FS::NotFormatted);
@ -175,6 +180,8 @@ void SaveDataArchive::format(const FSPath& path, const ArchiveBase::FormatInfo&
// Write format info on disk
IOFile file(formatInfoPath, "wb");
file.writeBytes(&info, sizeof(info));
file.flush();
file.close();
}
Rust::Result<ArchiveBase*, HorizonResult> SaveDataArchive::openArchive(const FSPath& path) {

View file

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

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

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

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

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

View file

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

View file

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

View file

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

View file

@ -3,9 +3,10 @@
#include "kernel_types.hpp"
#include "cpu.hpp"
Kernel::Kernel(CPU& cpu, Memory& mem, GPU& gpu)
: cpu(cpu), regs(cpu.regs()), mem(mem), handleCounter(0), serviceManager(regs, mem, gpu, currentProcess, *this) {
Kernel::Kernel(CPU& cpu, Memory& mem, GPU& gpu, const EmulatorConfig& config)
: cpu(cpu), regs(cpu.regs()), mem(mem), handleCounter(0), serviceManager(regs, mem, gpu, currentProcess, *this, config) {
objects.reserve(512); // Make room for a few objects to avoid further memory allocs later
mutexHandles.reserve(8);
portHandles.reserve(32);
threadIndices.reserve(appResourceLimits.maxThreads);
@ -34,6 +35,8 @@ void Kernel::serviceSVC(u32 svc) {
case 0x0A: svcSleepThread(); break;
case 0x0B: getThreadPriority(); break;
case 0x0C: setThreadPriority(); break;
case 0x0F: getThreadIdealProcessor(); break;
case 0x11: getCurrentProcessorNumber(); break;
case 0x13: svcCreateMutex(); break;
case 0x14: svcReleaseMutex(); break;
case 0x15: svcCreateSemaphore(); break;
@ -41,6 +44,10 @@ void Kernel::serviceSVC(u32 svc) {
case 0x17: svcCreateEvent(); break;
case 0x18: svcSignalEvent(); break;
case 0x19: svcClearEvent(); break;
case 0x1A: svcCreateTimer(); break;
case 0x1B: svcSetTimer(); break;
case 0x1C: svcCancelTimer(); break;
case 0x1D: svcClearTimer(); break;
case 0x1E: createMemoryBlock(); break;
case 0x1F: mapMemoryBlock(); break;
case 0x21: createAddressArbiter(); break;
@ -50,6 +57,7 @@ void Kernel::serviceSVC(u32 svc) {
case 0x25: waitSynchronizationN(); break;
case 0x27: duplicateHandle(); break;
case 0x28: getSystemTick(); break;
case 0x2A: getSystemInfo(); break;
case 0x2B: getProcessInfo(); break;
case 0x2D: connectToPort(); break;
case 0x32: sendSyncRequest(); break;
@ -61,6 +69,8 @@ void Kernel::serviceSVC(u32 svc) {
case 0x3D: outputDebugString(); break;
default: Helpers::panic("Unimplemented svc: %X @ %08X", svc, regs[15]); break;
}
evalReschedule();
}
void Kernel::setVersion(u8 major, u8 minor) {
@ -136,10 +146,13 @@ void Kernel::reset() {
deleteObjectData(object);
}
objects.clear();
mutexHandles.clear();
portHandles.clear();
threadIndices.clear();
serviceManager.reset();
needReschedule = false;
// Allocate handle #0 to a dummy object and make a main process object
makeObject(KernelObjectType::Dummy);
currentProcess = makeProcess(1); // Use ID = 1 for main process
@ -147,7 +160,7 @@ void Kernel::reset() {
// Make main thread object. We do not have to set the entrypoint and SP for it as the ROM loader does.
// Main thread seems to have a priority of 0x30. TODO: This creates a dummy context for thread 0,
// which is thankfully not used. Maybe we should prevent this
mainThread = makeThread(0, VirtualAddrs::StackTop, 0x30, -2, 0, ThreadStatus::Running);
mainThread = makeThread(0, VirtualAddrs::StackTop, 0x30, ProcessorID::Default, 0, ThreadStatus::Running);
currentThreadIndex = 0;
setupIdleThread();
@ -249,6 +262,99 @@ void Kernel::duplicateHandle() {
}
}
namespace SystemInfoType {
enum : u32 {
MemoryInformation = 0,
// Gets information related to Citra (We don't implement this, we just report this emulator is not Citra)
CitraInformation = 0x20000,
// Gets information related to this emulator
PandaInformation = 0x20001,
};
};
namespace CitraInfoType {
enum : u32 {
IsCitra = 0,
BuildName = 10, // (ie: Nightly, Canary).
BuildVersion = 11, // Build version.
BuildDate1 = 20, // Build date first 7 characters.
BuildDate2 = 21, // Build date next 7 characters.
BuildDate3 = 22, // Build date next 7 characters.
BuildDate4 = 23, // Build date last 7 characters.
BuildBranch1 = 30, // Git branch first 7 characters.
BuildBranch2 = 31, // Git branch last 7 characters.
BuildDesc1 = 40, // Git description (commit) first 7 characters.
BuildDesc2 = 41, // Git description (commit) last 7 characters.
};
}
namespace PandaInfoType {
enum : u32 {
IsPanda = 0,
};
}
void Kernel::getSystemInfo() {
const u32 infoType = regs[1];
const u32 subtype = regs[2];
log("GetSystemInfo (type = %X, subtype = %X)\n", infoType, subtype);
regs[0] = Result::Success;
switch (infoType) {
case SystemInfoType::MemoryInformation: {
switch (subtype) {
// Total used memory size in the APPLICATION memory region
case 1:
regs[1] = mem.getUsedUserMem();
regs[2] = 0;
break;
default:
Helpers::panic("GetSystemInfo: Unknown MemoryInformation subtype %x\n", subtype);
regs[0] = Result::FailurePlaceholder;
break;
}
break;
}
case SystemInfoType::CitraInformation: {
switch (subtype) {
case CitraInfoType::IsCitra:
// Report that we're not Citra
regs[1] = 0;
regs[2] = 0;
break;
default:
Helpers::warn("GetSystemInfo: Unknown CitraInformation subtype %x\n", subtype);
regs[0] = Result::FailurePlaceholder;
break;
}
break;
}
case SystemInfoType::PandaInformation: {
switch (subtype) {
case PandaInfoType::IsPanda:
// This is indeed us, set output to 1
regs[1] = 1;
regs[2] = 0;
break;
default:
Helpers::warn("GetSystemInfo: Unknown PandaInformation subtype %x\n", subtype);
regs[0] = Result::FailurePlaceholder;
break;
}
break;
}
default: Helpers::panic("GetSystemInfo: Unknown system info type: %x (subtype: %x)\n", infoType, subtype); break;
}
}
std::string Kernel::getProcessName(u32 pid) {
if (pid == KernelHandles::CurrentProcess) {
return "current";

View file

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

View file

@ -76,6 +76,11 @@ void Kernel::sendSyncRequest() {
u32 messagePointer = getTLSPointer() + 0x80; // The message is stored starting at TLS+0x80
logSVC("SendSyncRequest(session handle = %X)\n", handle);
// Service calls via SendSyncRequest and file access needs to put the caller to sleep for a given amount of time
// To make sure that the other threads don't get starved. Various games rely on this (including Sonic Boom: Shattering Crystal it seems)
constexpr u64 syncRequestDelayNs = 39000;
sleepThread(syncRequestDelayNs);
// The sync request is being sent at a service rather than whatever port, so have the service manager intercept it
if (KernelHandles::isServiceHandle(handle)) {
// The service call might cause a reschedule and change threads. Hence, set r0 before executing the service call
@ -104,7 +109,7 @@ void Kernel::sendSyncRequest() {
// If we're actually communicating with a port
const auto session = getObject(handle, KernelObjectType::Session);
if (session == nullptr) [[unlikely]] {
Helpers::panic("SendSyncRequest: Invalid handle");
Helpers::warn("SendSyncRequest: Invalid handle");
regs[0] = Result::Kernel::InvalidHandle;
return;
}

View file

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

View file

@ -82,37 +82,31 @@ std::optional<int> Kernel::getNextThread() {
return std::nullopt;
}
void Kernel::switchToNextThread() {
// See if there is a higher priority, ready thread and switch to that
void Kernel::rescheduleThreads() {
Thread& current = threads[currentThreadIndex]; // Current running thread
// If the current thread is running and hasn't gone to sleep or whatever, set it to Ready instead of Running
// So that getNextThread will evaluate it properly
if (current.status == ThreadStatus::Running) {
current.status = ThreadStatus::Ready;
}
ThreadStatus currentStatus = current.status;
std::optional<int> newThreadIndex = getNextThread();
if (!newThreadIndex.has_value()) {
log("Kernel tried to switch to the next thread but none found. Switching to random thread\n");
assert(aliveThreadCount != 0);
Helpers::panic("rpog");
int index;
do {
index = rand() % threadCount;
} while (threads[index].status == ThreadStatus::Dead); // TODO: Pray this doesn't hang
switchThread(index);
} else {
// Case 1: A thread can run
if (newThreadIndex.has_value()) {
switchThread(newThreadIndex.value());
}
}
// See if there;s a higher priority, ready thread and switch to that
void Kernel::rescheduleThreads() {
std::optional<int> newThreadIndex = getNextThread();
if (newThreadIndex.has_value() && newThreadIndex.value() != currentThreadIndex) {
threads[currentThreadIndex].status = ThreadStatus::Ready;
switchThread(newThreadIndex.value());
// Case 2: No other thread can run, straight to the idle thread
else {
switchThread(idleThreadIndex);
}
}
// Internal OS function to spawn a thread
Handle Kernel::makeThread(u32 entrypoint, u32 initialSP, u32 priority, s32 id, u32 arg, ThreadStatus status) {
Handle Kernel::makeThread(u32 entrypoint, u32 initialSP, u32 priority, ProcessorID id, u32 arg, ThreadStatus status) {
int index; // Index of the created thread in the threads array
if (threadCount < appResourceLimits.maxThreads) [[likely]] { // If we have not yet created over too many threads
@ -174,6 +168,11 @@ Handle Kernel::makeMutex(bool locked) {
moo->ownerThread = currentThreadIndex;
}
// Push the new mutex to our list of mutex handles
// We need a list of mutex handles so that when a thread is killed, we can look which mutexes from this list the thread owns and free them
// Alternatively this could be a per-thread list, but I don't want to push_back and remove on every mutex lock and release
// Since some mutexes like the APT service mutex are locked and unlocked constantly, while ExitThread is a relatively "rare" SVC
mutexHandles.push_back(ret);
return ret;
}
@ -184,6 +183,7 @@ void Kernel::releaseMutex(Mutex* moo) {
// If the lock count reached 0 then the thread no longer owns the mootex and it can be given to a new one
if (moo->lockCount == 0) {
moo->locked = false;
if (moo->waitlist != 0) {
int index = wakeupOneThread(moo->waitlist, moo->handle); // Wake up one thread and get its index
moo->waitlist ^= (1ull << index); // Remove thread from waitlist
@ -194,7 +194,7 @@ void Kernel::releaseMutex(Mutex* moo) {
moo->ownerThread = index;
}
rescheduleThreads();
requireReschedule();
}
}
@ -210,7 +210,7 @@ void Kernel::sleepThreadOnArbiter(u32 waitingAddress) {
t.status = ThreadStatus::WaitArbiter;
t.waitingAddress = waitingAddress;
switchToNextThread();
requireReschedule();
}
// Acquires an object that is **ready to be acquired** without waiting on it
@ -226,7 +226,13 @@ void Kernel::acquireSyncObject(KernelObject* object, const Thread& thread) {
case KernelObjectType::Mutex: {
Mutex* moo = object->getData<Mutex>();
moo->locked = true; // Set locked to true, whether it's false or not because who cares
// Only reschedule if we're acquiring the mutex for the first time
if (!moo->locked) {
moo->locked = true;
requireReschedule();
}
// Increment lock count by 1. If a thread acquires a mootex multiple times, it needs to release it until count == 0
// For the mootex to be free.
moo->lockCount++;
@ -338,20 +344,31 @@ void Kernel::wakeupAllThreads(u64 waitlist, Handle handle) {
void Kernel::sleepThread(s64 ns) {
if (ns < 0) {
Helpers::panic("Sleeping a thread for a negative amount of ns");
} else if (ns == 0) { // Used when we want to force a thread switch
std::optional<int> newThreadIndex = getNextThread();
// If there's no other thread waiting, don't bother yielding
if (newThreadIndex.has_value()) {
threads[currentThreadIndex].status = ThreadStatus::Ready;
switchThread(newThreadIndex.value());
}
} else { // If we're sleeping for > 0 ns
} else if (ns == 0) {
// TODO: This is garbage, but it works so eh we can keep it for now
Thread& t = threads[currentThreadIndex];
// See if a thread other than this and the idle thread is waiting to run by temp marking the current function as dead and searching
// If there is another thread to run, then run it. Otherwise, go back to this thread, not to the idle thread
t.status = ThreadStatus::Dead;
auto nextThreadIndex = getNextThread();
t.status = ThreadStatus::Ready;
if (nextThreadIndex.has_value()) {
const auto index = nextThreadIndex.value();
if (index != idleThreadIndex) {
switchThread(index);
}
}
} else { // If we're sleeping for >= 0 ns
Thread& t = threads[currentThreadIndex];
t.status = ThreadStatus::WaitSleep;
t.waitingNanoseconds = ns;
t.sleepTick = cpu.getTicks();
switchToNextThread();
requireReschedule();
}
}
@ -372,9 +389,13 @@ void Kernel::createThread() {
return;
}
if (id < -2 || id > 3) {
Helpers::panic("Invalid processor ID in CreateThread");
}
regs[0] = Result::Success;
regs[1] = makeThread(entrypoint, initialSP, priority, id, arg, ThreadStatus::Ready);
rescheduleThreads();
regs[1] = makeThread(entrypoint, initialSP, priority, static_cast<ProcessorID>(id), arg, ThreadStatus::Ready);
requireReschedule();
}
// void SleepThread(s64 nanoseconds)
@ -424,6 +445,15 @@ void Kernel::getThreadPriority() {
}
}
void Kernel::getThreadIdealProcessor() {
const Handle handle = regs[1]; // Thread handle
logSVC("GetThreadIdealProcessor (handle = %X)\n", handle);
// TODO: Not documented what this is or what it does. Citra doesn't implement it at all. Return AppCore as the ideal processor for now
regs[0] = Result::Success;
regs[1] = static_cast<u32>(ProcessorID::AppCore);
}
void Kernel::setThreadPriority() {
const Handle handle = regs[0];
const u32 priority = regs[1];
@ -448,12 +478,56 @@ void Kernel::setThreadPriority() {
}
}
sortThreads();
rescheduleThreads();
requireReschedule();
}
void Kernel::getCurrentProcessorNumber() {
logSVC("GetCurrentProcessorNumber()\n");
const ProcessorID id = threads[currentThreadIndex].processorID;
s32 ret;
// Until we properly implement per-core schedulers, return whatever processor ID passed to svcCreateThread
switch (id) {
// TODO: This is picked from exheader
case ProcessorID::Default:
ret = static_cast<s32>(ProcessorID::AppCore);
break;
case ProcessorID::AllCPUs:
ret = static_cast<s32>(ProcessorID::AppCore);
Helpers::warn("GetCurrentProcessorNumber on thread created to run on all CPUs...?\n");
break;
default: ret = static_cast<s32>(id); break;
}
if (ret != static_cast<s32>(ProcessorID::AppCore)) {
Helpers::warn("GetCurrentProcessorNumber: Thread not running on appcore\n");
}
regs[0] = static_cast<u32>(ret);
}
void Kernel::exitThread() {
logSVC("ExitThread\n");
// Find which mutexes this thread owns, release them
for (auto handle : mutexHandles) {
KernelObject* object = getObject(handle, KernelObjectType::Mutex);
// Make sure that the handle actually matches to a mutex, and if our exiting thread owns the mutex, release it
if (object != nullptr) {
Mutex* moo = object->getData<Mutex>();
if (moo->locked && moo->ownerThread == currentThreadIndex) {
// Release the mutex by setting lock count to 1 and releasing it once. We set lock count to 1 since it's a recursive mutex
// Therefore if its lock count was > 1, simply calling releaseMutex would not fully release it
moo->lockCount = 1;
releaseMutex(moo);
}
}
}
// Remove the index of this thread from the thread indices vector
for (int i = 0; i < threadIndices.size(); i++) {
if (threadIndices[i] == currentThreadIndex)
@ -472,7 +546,7 @@ void Kernel::exitThread() {
t.threadsWaitingForTermination = 0; // No other threads waiting
}
switchToNextThread();
requireReschedule();
}
void Kernel::svcCreateMutex() {

View file

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

View file

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

Some files were not shown because too many files have changed in this diff Show more