Merge remote-tracking branch 'upstream/master' into CRO

This commit is contained in:
Nomi 2023-09-07 20:06:48 +02:00
commit d50c94cbdc
48 changed files with 4278 additions and 3203 deletions

15
.github/Alber.desktop vendored Normal file
View file

@ -0,0 +1,15 @@
[Desktop Entry]
Version=1.0
Type=Application
Name=Alber
GenericName=3DS Emulator
GenericName[fr]=Émulateur 3DS
Comment=Nintendo 3DS video game console emulator
Comment[fr]=Émulateur de console de jeu Nintendo 3DS
Icon=Alber
TryExec=Alber
Exec=Alber %f
Categories=Game;Emulator;
MimeType=application/x-ctr-3dsx;application/x-ctr-cci;application/x-ctr-cia;application/x-ctr-cxi;
Keywords=3DS;Nintendo;
PrefersNonDefaultGPU=true

6
.github/linux-appimage.sh vendored Executable file
View file

@ -0,0 +1,6 @@
# Prepare Tools for building the AppImage
wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
chmod a+x linuxdeploy-x86_64.AppImage
# Build AppImage
./linuxdeploy-x86_64.AppImage --appdir AppDir -d ./.github/Alber.desktop -e ./build/Alber -i ./docs/img/Alber.png --output appimage

View file

@ -0,0 +1,66 @@
name: Linux AppImage 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-20.04
steps:
- uses: actions/checkout@v2
- 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 libfuse2
- name: Install newer Clang
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x ./llvm.sh
sudo ./llvm.sh 16
- name: Install newer CMake
run: |
sudo curl -s https://apt.kitware.com/keys/kitware-archive-latest.asc | gpg --dearmor | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 42D5A192B819C5DA
sudo add-apt-repository -y 'deb https://apt.kitware.com/ubuntu/ focal main'
sudo apt-get update
sudo apt-get install cmake
- name: Setup Vulkan SDK
run: |
wget -qO - http://packages.lunarg.com/lunarg-signing-key-pub.asc | sudo apt-key add -
sudo wget -qO /etc/apt/sources.list.d/lunarg-vulkan-focal.list http://packages.lunarg.com/vulkan/lunarg-vulkan-focal.list
sudo apt update
sudo apt install vulkan-sdk
- 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-16 -DCMAKE_CXX_COMPILER=clang++-16 -DENABLE_USER_BUILD=ON
- name: Build
# Build your program with the given configuration
run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}}
- name: Run AppImage packaging script
run: ./.github/linux-appimage.sh
- name: Upload executable
uses: actions/upload-artifact@v2
with:
name: Linux executable
path: './Alber-x86_64.AppImage'

View file

@ -144,7 +144,7 @@ set(PICA_SOURCE_FILES src/core/PICA/gpu.cpp src/core/PICA/regs.cpp src/core/PICA
src/core/PICA/dynapica/shader_rec_emitter_x64.cpp src/core/PICA/pica_hash.cpp
)
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(LOADER_SOURCE_FILES src/core/loader/elf.cpp src/core/loader/ncsd.cpp src/core/loader/ncch.cpp src/core/loader/3dsx.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/romfs.cpp
src/core/fs/ivfc.cpp src/core/fs/archive_user_save_data.cpp
@ -161,7 +161,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.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
include/logger.hpp include/loader/ncch.hpp include/loader/ncsd.hpp include/loader/3dsx.hpp include/io_file.hpp
include/loader/lz77.hpp include/fs/archive_base.hpp include/fs/archive_self_ncch.hpp
include/services/dsp.hpp include/services/cfg.hpp include/services/region_codes.hpp
include/fs/archive_save_data.hpp include/fs/archive_sdmc.hpp include/services/ptm.hpp
@ -273,18 +273,28 @@ if(ENABLE_VULKAN)
"src/host_shaders/vulkan_display.vert"
)
foreach( HOST_SHADER_SOURCE ${RENDERER_VK_HOST_SHADERS_SOURCE} )
get_filename_component( FILE_NAME ${HOST_SHADER_SOURCE} NAME )
set( HOST_SHADER_SPIRV "${PROJECT_BINARY_DIR}/host_shaders/${FILE_NAME}.spv" )
add_custom_command(
OUTPUT ${HOST_SHADER_SPIRV}
COMMAND ${CMAKE_COMMAND} -E make_directory "${PROJECT_BINARY_DIR}/host_shaders/"
COMMAND Vulkan::glslangValidator -t --target-env vulkan1.1 -g -V "${PROJECT_SOURCE_DIR}/${HOST_SHADER_SOURCE}" -o ${HOST_SHADER_SPIRV}
#COMMAND ${SPIRV_OPT} -O ${HOST_SHADER_SPIRV} -o ${HOST_SHADER_SPIRV}
DEPENDS ${HOST_SHADER_SOURCE}
)
list( APPEND RENDERER_VK_HOST_SHADERS_SPIRV ${HOST_SHADER_SPIRV} )
endforeach()
set( RENDERER_VK_HOST_SHADERS_FLAGS -e main --target-env vulkan1.1)
if(GPU_DEBUG_INFO)
# generate nonsemantic shader debug information with source
set( RENDERER_VK_HOST_SHADERS_FLAGS ${RENDERER_VK_HOST_SHADERS_FLAGS} -gVS)
else()
set( RENDERER_VK_HOST_SHADERS_FLAGS ${RENDERER_VK_HOST_SHADERS_FLAGS} -g0)
endif()
# Compile each vulkan shader into an .spv file
foreach( HOST_SHADER_SOURCE ${RENDERER_VK_HOST_SHADERS_SOURCE} )
get_filename_component( FILE_NAME ${HOST_SHADER_SOURCE} NAME )
set( HOST_SHADER_SPIRV "${PROJECT_BINARY_DIR}/host_shaders/${FILE_NAME}.spv" )
add_custom_command(
OUTPUT ${HOST_SHADER_SPIRV}
COMMAND ${CMAKE_COMMAND} -E make_directory "${PROJECT_BINARY_DIR}/host_shaders/"
COMMAND Vulkan::glslangValidator ${RENDERER_VK_HOST_SHADERS_FLAGS} -V "${PROJECT_SOURCE_DIR}/${HOST_SHADER_SOURCE}" -o ${HOST_SHADER_SPIRV}
DEPENDS ${HOST_SHADER_SOURCE}
)
list( APPEND RENDERER_VK_HOST_SHADERS_SPIRV ${HOST_SHADER_SPIRV} )
endforeach()
cmrc_add_resource_library(
resources_renderer_vk

BIN
docs/img/Alber.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 MiB

View file

@ -170,6 +170,8 @@ public:
return env.totalTicks;
}
void clearCache() { jit->ClearCache(); }
void runFrame() {
env.ticksLeft = ticksPerSec / 60;

View file

@ -24,6 +24,7 @@ enum class ROMType {
ELF,
NCSD,
CXI,
HB_3DSX,
};
class Emulator {
@ -99,6 +100,7 @@ class Emulator {
bool loadROM(const std::filesystem::path& path);
bool loadNCSD(const std::filesystem::path& path, ROMType type);
bool load3DSX(const std::filesystem::path& path);
bool loadELF(const std::filesystem::path& path);
bool loadELF(std::ifstream& file);
void initGraphicsContext();

View file

@ -251,4 +251,11 @@ public:
virtual std::optional<u32> readFile(FileSession* file, u64 offset, u32 size, u32 dataPointer) = 0;
ArchiveBase(Memory& mem) : mem(mem) {}
};
struct ArchiveResource {
u32 sectorSize; // Size of a sector in bytes
u32 clusterSize; // Size of a cluster in bytes
u32 partitionCapacityInClusters;
u32 freeSpaceInClusters;
};

View file

@ -9,6 +9,7 @@ public:
u64 getFreeBytes() override { Helpers::panic("ExtSaveData::GetFreeBytes unimplemented"); return 0; }
std::string name() override { return "ExtSaveData::" + backingFolder; }
HorizonResult createDirectory(const FSPath& path) override;
HorizonResult createFile(const FSPath& path, u64 size) override;
HorizonResult deleteFile(const FSPath& path) override;

View file

@ -8,13 +8,16 @@ class SDMCArchive : public ArchiveBase {
public:
SDMCArchive(Memory& mem) : ArchiveBase(mem) {}
u64 getFreeBytes() override { Helpers::panic("SDMC::GetFreeBytes unimplemented"); return 0; }
u64 getFreeBytes() override { return 1_GB; }
std::string name() override { return "SDMC"; }
HorizonResult createFile(const FSPath& path, u64 size) override;
HorizonResult deleteFile(const FSPath& path) override;
HorizonResult createDirectory(const FSPath& path) override;
Rust::Result<ArchiveBase*, HorizonResult> openArchive(const FSPath& path) override;
Rust::Result<DirectorySession, HorizonResult> openDirectory(const FSPath& path) override;
FileDescriptor openFile(const FSPath& path, const FilePerms& perms) override;
std::optional<u32> readFile(FileSession* file, u64 offset, u32 size, u32 dataPointer) override;
};

View file

@ -18,7 +18,8 @@ public:
// Returns whether the cart has a RomFS
bool hasRomFS() {
auto cxi = mem.getCXI();
return (cxi != nullptr && cxi->hasRomFS());
auto hb3dsx = mem.get3DSX();
return (cxi != nullptr && cxi->hasRomFS()) || (hb3dsx != nullptr && hb3dsx->hasRomFs());
}
// Returns whether the cart has an ExeFS (All executable carts should have an ExeFS. This is just here to be safe)

File diff suppressed because it is too large Load diff

View file

@ -137,7 +137,7 @@ namespace Helpers {
return getBits<offset, bits, ValueT, ValueT>(value);
}
#if defined(HELPERS_APPLE_CLANG) || defined(__ANDROID__)
#if defined(HELPERS_APPLE_CLANG) || defined(__ANDROID__) || !defined(__cpp_lib_bit_cast)
template <class To, class From>
constexpr To bit_cast(const From& from) noexcept {
return *reinterpret_cast<const To*>(&from);

View file

@ -83,6 +83,9 @@ private:
bool canThreadRun(const Thread& t);
bool shouldWaitOnObject(KernelObject* object);
void releaseMutex(Mutex* moo);
void cancelTimer(Timer* timer);
void signalTimer(Handle timerHandle, Timer* timer);
void updateTimer(Handle timerHandle, Timer* timer);
// Wake up the thread with the highest priority out of all threads in the waitlist
// Returns the index of the woken up thread
@ -226,4 +229,5 @@ public:
void sendGPUInterrupt(GPUInterrupt type) { serviceManager.sendGPUInterrupt(type); }
void signalDSPEvents() { serviceManager.signalDSPEvents(); }
void clearInstructionCache();
};

View file

@ -173,6 +173,19 @@ struct Semaphore {
Semaphore(s32 initialCount, s32 maximumCount) : availableCount(initialCount), maximumCount(maximumCount), waitlist(0) {}
};
struct Timer {
u64 waitlist; // Refer to the getWaitlist function below for documentation
ResetType resetType = ResetType::OneShot;
u64 startTick; // CPU tick the timer started
u64 currentDelay; // Number of ns until the timer fires next time
u64 interval; // Number of ns until the timer fires for the second and future times
bool fired; // Has this timer been signalled?
bool running; // Is this timer running or stopped?
Timer(ResetType type) : resetType(type), startTick(0), currentDelay(0), interval(0), waitlist(0), fired(false), running(false) {}
};
struct MemoryBlock {
u32 addr = 0;
u32 size = 0;
@ -206,21 +219,23 @@ struct KernelObject {
}
// Retrieves a reference to the waitlist for a specified object
// We return a reference because this function is only called in the kernel threading internals
// We want the kernel to be able to easily manage waitlists, by reading/parsing them or setting/clearing bits.
// As we mention in the definition of the "Event" struct, the format for wailists is very simple and made to be efficient.
// Each bit corresponds to a thread index and denotes whether the corresponding thread is waiting on this object
// For example if bit 0 of the wait list is set, then the thread with index 0 is waiting on our object
u64& getWaitlist() {
// This code is actually kinda trash but eh good enough
switch (type) {
case KernelObjectType::Event: return getData<Event>()->waitlist;
case KernelObjectType::Mutex: return getData<Mutex>()->waitlist;
case KernelObjectType::Semaphore: return getData<Mutex>()->waitlist;
case KernelObjectType::Thread: return getData<Thread>()->threadsWaitingForTermination;
// This should be unreachable once we fully implement sync objects
default: [[unlikely]]
// We return a reference because this function is only called in the kernel threading internals
// We want the kernel to be able to easily manage waitlists, by reading/parsing them or setting/clearing bits.
// As we mention in the definition of the "Event" struct, the format for wailists is very simple and made to be efficient.
// Each bit corresponds to a thread index and denotes whether the corresponding thread is waiting on this object
// For example if bit 0 of the wait list is set, then the thread with index 0 is waiting on our object
u64& getWaitlist() {
// This code is actually kinda trash but eh good enough
switch (type) {
case KernelObjectType::Event: return getData<Event>()->waitlist;
case KernelObjectType::Mutex: return getData<Mutex>()->waitlist;
case KernelObjectType::Semaphore: return getData<Mutex>()->waitlist;
case KernelObjectType::Thread: return getData<Thread>()->threadsWaitingForTermination;
case KernelObjectType::Timer: return getData<Timer>()->waitlist;
// This should be unreachable once we fully implement sync objects
default: [[unlikely]]
Helpers::panic("Called GetWaitList on kernel object without a waitlist (Type: %s)", getTypeName());
}
}
}
}
};

80
include/loader/3dsx.hpp Normal file
View file

@ -0,0 +1,80 @@
#pragma once
#include <array>
#include "helpers.hpp"
#include "io_file.hpp"
#include "loader/ncch.hpp"
struct HB3DSX {
// File layout:
// - File header
// - Code, rodata and data relocation table headers
// - Code segment
// - Rodata segment
// - Loadable (non-BSS) part of the data segment
// - Code relocation table
// - Rodata relocation table
// - Data relocation table
// Memory layout before relocations are applied:
// [0..codeSegSize) -> code segment
// [codeSegSize..rodataSegSize) -> rodata segment
// [rodataSegSize..dataSegSize) -> data segment
// Memory layout after relocations are applied: well, however the loader sets it up :)
// The entrypoint is always the start of the code segment.
// The BSS section must be cleared manually by the application.
// File header
struct Header {
// minus char magic[4]
u16 headerSize;
u16 relocHeaderSize;
u32 formatVer;
u32 flags;
// Sizes of the code, rodata and data segments +
// size of the BSS section (uninitialized latter half of the data segment)
u32 codeSegSize, rodataSegSize, dataSegSize, bssSize;
};
// Relocation header: all fields (even extra unknown fields) are guaranteed to be relocation counts.
struct RelocHeader {
u32 absoluteCount; // # of absolute relocations (that is, fix address to post-relocation memory layout)
u32 relativeCount; // # of cross-segment relative relocations (that is, 32bit signed offsets that need to be patched)
// more?
// Relocations are written in this order:
// - Absolute relocs
// - Relative relocs
};
enum class RelocType {
Absolute,
Relative,
};
// Relocation entry: from the current pointer, skip X words and patch Y words
struct Reloc {
u16 skip, patch;
};
// _prm structure
static constexpr std::array<char, 4> PRM_MAGIC = {'_', 'P', 'R', 'M'};
struct PrmStruct {
char magic[4];
u32 pSrvOverride;
u32 aptAppId;
u32 heapSize, linearHeapSize;
u32 pArgList;
u32 runFlags;
};
IOFile file;
static constexpr u32 entrypoint = 0x00100000; // Initial ARM11 PC
u32 romFSSize = 0;
u32 romFSOffset = 0;
bool hasRomFs() const;
std::pair<bool, std::size_t> readRomFSBytes(void *dst, std::size_t offset, std::size_t size);
};

View file

@ -11,6 +11,7 @@
#include "handles.hpp"
#include "helpers.hpp"
#include "loader/ncsd.hpp"
#include "loader/3dsx.hpp"
#include "services/region_codes.hpp"
namespace PhysicalAddrs {
@ -167,10 +168,12 @@ public:
void* getReadPointer(u32 address);
void* getWritePointer(u32 address);
std::optional<u32> loadELF(std::ifstream& file);
std::optional<u32> load3DSX(const std::filesystem::path& path);
std::optional<NCSD> loadNCSD(Crypto::AESEngine& aesEngine, const std::filesystem::path& path);
std::optional<NCSD> loadCXI(Crypto::AESEngine& aesEngine, const std::filesystem::path& path);
bool mapCXI(NCSD& ncsd, NCCH& cxi);
bool map3DSX(HB3DSX& hb3dsx, const HB3DSX::Header& header);
u8 read8(u32 vaddr);
u16 read16(u32 vaddr);
@ -221,6 +224,14 @@ public:
}
}
HB3DSX* get3DSX() {
if (loaded3DSX.has_value()) {
return &loaded3DSX.value();
} else {
return nullptr;
}
}
// Returns whether "addr" is aligned to a page (4096 byte) boundary
static constexpr bool isAligned(u32 addr) {
return (addr & pageMask) == 0;
@ -256,6 +267,7 @@ public:
// Backup of the game's CXI partition info, if any
std::optional<NCCH> loadedCXI = std::nullopt;
std::optional<HB3DSX> loaded3DSX = std::nullopt;
// File handle for reading the loaded ncch
IOFile CXIFile;

View file

@ -25,6 +25,9 @@ class CAMService {
void driverInitialize(u32 messagePointer);
void getMaxLines(u32 messagePointer);
void getBufferErrorInterruptEvent(u32 messagePointer);
void setContrast(u32 messagePointer);
void setFrameRate(u32 messagePointer);
void setTransferLines(u32 messagePointer);
public:
CAMService(Memory& mem, Kernel& kernel) : mem(mem), kernel(kernel) {}

View file

@ -1,4 +1,5 @@
#pragma once
#include "config.hpp"
#include "fs/archive_ext_save_data.hpp"
#include "fs/archive_ncch.hpp"
#include "fs/archive_save_data.hpp"
@ -9,7 +10,6 @@
#include "kernel_types.hpp"
#include "logger.hpp"
#include "memory.hpp"
#include "result/result.hpp"
// Yay, more circular dependencies
class Kernel;
@ -40,7 +40,10 @@ class FSService {
std::optional<Handle> openFileHandle(ArchiveBase* archive, const FSPath& path, const FSPath& archivePath, const FilePerms& perms);
FSPath readPath(u32 type, u32 pointer, u32 size);
const EmulatorConfig& config;
// Service commands
void abnegateAccessRight(u32 messagePointer);
void createDirectory(u32 messagePointer);
void createExtSaveData(u32 messagePointer);
void createFile(u32 messagePointer);
@ -50,12 +53,12 @@ class FSService {
void deleteFile(u32 messagePointer);
void formatSaveData(u32 messagePointer);
void formatThisUserSaveData(u32 messagePointer);
void getArchiveResource(u32 messagePointer);
void getFreeBytes(u32 messagePointer);
void getFormatInfo(u32 messagePointer);
void getPriority(u32 messagePointer);
void getThisSaveDataSecureValue(u32 messagePointer);
void theGameboyVCFunction(u32 messagePointer);
void initialize(u32 messagePointer);
void initializeWithSdkVersion(u32 messagePointer);
void isSdmcDetected(u32 messagePointer);
@ -64,15 +67,17 @@ class FSService {
void openDirectory(u32 messagePointer);
void openFile(u32 messagePointer);
void openFileDirectly(u32 messagePointer);
void setArchivePriority(u32 messagePointer);
void setPriority(u32 messagePointer);
void setThisSaveDataSecureValue(u32 messagePointer);
// Used for set/get priority: Not sure what sort of priority this is referring to
u32 priority;
public:
FSService(Memory& mem, Kernel& kernel)
FSService(Memory& mem, Kernel& kernel, const EmulatorConfig& config)
: mem(mem), saveData(mem), sharedExtSaveData_nand(mem, "../SharedFiles/NAND", true), extSaveData_sdmc(mem, "SDMC"), sdmc(mem), selfNcch(mem),
ncch(mem), userSaveData1(mem, ArchiveID::UserSaveData1), userSaveData2(mem, ArchiveID::UserSaveData2), kernel(kernel) {}
ncch(mem), userSaveData1(mem, ArchiveID::UserSaveData1), userSaveData2(mem, ArchiveID::UserSaveData2), kernel(kernel), config(config) {}
void reset();
void handleSyncRequest(u32 messagePointer);

View file

@ -12,7 +12,9 @@ class HTTPService {
bool initialized = false;
// Service commands
void createRootCertChain(u32 messagePointer);
void initialize(u32 messagePointer);
void rootCertChainAddDefaultCert(u32 messagePointer);
public:
HTTPService(Memory& mem) : mem(mem) {}

View file

@ -17,6 +17,7 @@ class MICService {
// Service commands
void getEventHandle(u32 messagePointer);
void getGain(u32 messagePointer);
void getPower(u32 messagePointer);
void isSampling(u32 messagePointer);
void mapSharedMem(u32 messagePointer);
void setClamp(u32 messagePointer);

View file

@ -16,6 +16,7 @@ class PTMService {
// Service commands
void configureNew3DSCPU(u32 messagePointer);
void getAdapterState(u32 messagePointer);
void getBatteryChargeState(u32 messagePointer);
void getBatteryLevel(u32 messagePointer);
void getStepHistory(u32 messagePointer);
void getTotalStepCount(u32 messagePointer);

View file

@ -84,6 +84,7 @@ class ServiceManager {
void receiveNotification(u32 messagePointer);
void registerClient(u32 messagePointer);
void subscribe(u32 messagePointer);
void unsubscribe(u32 messagePointer);
public:
ServiceManager(std::span<u32, 16> regs, Memory& mem, GPU& gpu, u32& currentPID, Kernel& kernel, const EmulatorConfig& config);

View file

@ -64,10 +64,14 @@ class Y2RService {
// Service commands
void driverInitialize(u32 messagePointer);
void driverFinalize(u32 messagePointer);
void getTransferEndEvent(u32 messagePointer);
void getBlockAlignment(u32 messagePointer);
void getInputLines(u32 messagePointer);
void getInputLineWidth(u32 messagePointer);
void getOutputFormat(u32 messagePointer);
void isBusyConversion(u32 messagePointer);
void pingProcess(u32 messagePointer);
void setTransferEndInterrupt(u32 messagePointer);
void getTransferEndEvent(u32 messagePointer);
void setAlpha(u32 messagePointer);
void setBlockAlignment(u32 messagePointer);

View file

@ -17,14 +17,14 @@ You can download stable builds from the Releases tab, or you can download the la
|--------|------------|--------|
|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)|
|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 AppImage](https://nightly.link/wheremyfoodat/Panda3DS/workflows/Linux_AppImage_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, 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.
For documenting game compatibility, make sure to visit the [games list repository](https://github.com/Panda3DS-emu/Panda3DS-Games-List). For miscellaneous issues or more technical issues, feel free to use this repo's issues tab.
# 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:
@ -61,6 +61,7 @@ Panda3DS can load ROMs in the following formats:
- .3ds/.cci
- .cxi/.app
- .elf/.axf
- .3dsx
Both decrypted and encrypted dumps are supported. However for encrypted dumps you must provide your AES keys file by adding a `sysdata` folder to the emulator's app data directory with a file called `aes_keys.txt` including your keys. Currently .cia files are not supported yet (support is planned for the future), however if you want you can usually use Citra to extract the .app/.cxi file out of your .cia and run that.
@ -110,7 +111,7 @@ Panda3DS also supports controller input using the SDL2 GameController API.
# Support
If you find this project exciting and want to support the founder, check out [his Patreon page](https://www.patreon.com/wheremyfoodat)
<br>
Keep in mind, funding is only aimed to cover various life costs and support development. Panda3DS is a free product and on no occasion will official builds ever be made private or limited to sponsors! Any donation is much appreciated!
Keep in mind that funding is only aimed to cover various life costs and support development. Panda3DS is a free product and on no occasion will official builds ever be made private or limited to sponsors! Any donation is much appreciated!
Nintendo 3DS is a registered trademark of Nintendo Co., Ltd.

View file

@ -87,6 +87,27 @@ FileDescriptor ExtSaveDataArchive::openFile(const FSPath& path, const FilePerms&
return FileError;
}
HorizonResult ExtSaveDataArchive::createDirectory(const FSPath& path) {
if (path.type == PathType::UTF16) {
if (!isPathSafe<PathType::UTF16>(path)) {
Helpers::panic("Unsafe path in ExtSaveData::OpenFile");
}
fs::path p = IOFile::getAppData() / backingFolder;
p += fs::path(path.utf16_string).make_preferred();
if (fs::is_directory(p)) return Result::FS::AlreadyExists;
if (fs::is_regular_file(p)) {
Helpers::panic("File path passed to ExtSaveData::CreateDirectory");
}
bool success = fs::create_directory(p);
return success ? Result::Success : Result::FS::UnexpectedFileOrDir;
} else {
Helpers::panic("Unimplemented ExtSaveData::CreateDirectory");
}
}
std::string ExtSaveDataArchive::getExtSaveDataPathFromBinary(const FSPath& path) {
// TODO: Remove punning here
const u32 mediaType = *(u32*)&path.binary[0];

View file

@ -131,9 +131,6 @@ std::optional<u32> NCCHArchive::readFile(FileSession* file, u64 offset, u32 size
}
auto cxi = mem.getCXI();
IOFile& ioFile = mem.CXIFile;
NCCH::FSInfo fsInfo;
// Seek to file offset depending on if we're reading from RomFS, ExeFS, etc
switch (type) {
@ -144,7 +141,6 @@ std::optional<u32> NCCHArchive::readFile(FileSession* file, u64 offset, u32 size
Helpers::panic("Tried to read from NCCH with too big of an offset");
}
fsInfo = cxi->romFS;
offset += 0x1000;
break;
}
@ -154,7 +150,7 @@ std::optional<u32> NCCHArchive::readFile(FileSession* file, u64 offset, u32 size
}
std::unique_ptr<u8[]> data(new u8[size]);
auto [success, bytesRead] = cxi->readFromFile(ioFile, fsInfo, &data[0], offset, size);
auto [success, bytesRead] = cxi->readFromFile(mem.CXIFile, cxi->romFS, &data[0], offset, size);
if (!success) {
Helpers::panic("Failed to read from NCCH archive");

View file

@ -39,14 +39,17 @@ HorizonResult SaveDataArchive::createFile(const FSPath& path, u64 size) {
HorizonResult SaveDataArchive::createDirectory(const FSPath& path) {
if (path.type == PathType::UTF16) {
if (!isPathSafe<PathType::UTF16>(path))
if (!isPathSafe<PathType::UTF16>(path)) {
Helpers::panic("Unsafe path in SaveData::OpenFile");
}
fs::path p = IOFile::getAppData() / "SaveData";
p += fs::path(path.utf16_string).make_preferred();
if (fs::is_directory(p))
if (fs::is_directory(p)) {
return Result::FS::AlreadyExists;
}
if (fs::is_regular_file(p)) {
Helpers::panic("File path passed to SaveData::CreateDirectory");
}
@ -92,11 +95,13 @@ HorizonResult SaveDataArchive::deleteFile(const FSPath& path) {
FileDescriptor SaveDataArchive::openFile(const FSPath& path, const FilePerms& perms) {
if (path.type == PathType::UTF16) {
if (!isPathSafe<PathType::UTF16>(path))
if (!isPathSafe<PathType::UTF16>(path)) {
Helpers::panic("Unsafe path in SaveData::OpenFile");
}
if (perms.raw == 0 || (perms.create() && !perms.write()))
if (perms.raw == 0 || (perms.create() && !perms.write())) {
Helpers::panic("[SaveData] Unsupported flags for OpenFile");
}
fs::path p = IOFile::getAppData() / "SaveData";
p += fs::path(path.utf16_string).make_preferred();
@ -126,8 +131,9 @@ FileDescriptor SaveDataArchive::openFile(const FSPath& path, const FilePerms& pe
Rust::Result<DirectorySession, HorizonResult> SaveDataArchive::openDirectory(const FSPath& path) {
if (path.type == PathType::UTF16) {
if (!isPathSafe<PathType::UTF16>(path))
if (!isPathSafe<PathType::UTF16>(path)) {
Helpers::panic("Unsafe path in SaveData::OpenDirectory");
}
fs::path p = IOFile::getAppData() / "SaveData";
p += fs::path(path.utf16_string).make_preferred();

View file

@ -1,6 +1,8 @@
#include "fs/archive_sdmc.hpp"
#include <memory>
namespace fs = std::filesystem;
HorizonResult SDMCArchive::createFile(const FSPath& path, u64 size) {
Helpers::panic("[SDMC] CreateFile not yet supported");
return Result::Success;
@ -12,13 +14,123 @@ HorizonResult SDMCArchive::deleteFile(const FSPath& path) {
}
FileDescriptor SDMCArchive::openFile(const FSPath& path, const FilePerms& perms) {
printf("SDMCArchive::OpenFile: Failed");
return FileError;
FilePerms realPerms = perms;
// SD card always has read permission
realPerms.raw |= (1 << 0);
if ((realPerms.create() && !realPerms.write())) {
Helpers::panic("[SDMC] Unsupported flags for OpenFile");
}
std::filesystem::path p = IOFile::getAppData() / "SDMC";
switch (path.type) {
case PathType::ASCII:
if (!isPathSafe<PathType::ASCII>(path)) {
Helpers::panic("Unsafe path in SDMCArchive::OpenFile");
}
p += fs::path(path.string).make_preferred();
break;
case PathType::UTF16:
if (!isPathSafe<PathType::UTF16>(path)) {
Helpers::panic("Unsafe path in SDMCArchive::OpenFile");
}
p += fs::path(path.utf16_string).make_preferred();
break;
default: Helpers::panic("SDMCArchive::OpenFile: Failed. Path type: %d", path.type); return FileError;
}
const char* permString = perms.write() ? "r+b" : "rb";
if (fs::exists(p)) { // Return file descriptor if the file exists
IOFile file(p.string().c_str(), permString);
return file.isOpen() ? file.getHandle() : FileError;
} else {
// If the file is not found, create it if the create flag is on
if (realPerms.create()) {
IOFile file(p.string().c_str(), "wb"); // Create file
file.close(); // Close it
file.open(p.string().c_str(), permString); // Reopen with proper perms
return file.isOpen() ? file.getHandle() : FileError;
} else {
return FileError;
}
}
}
HorizonResult SDMCArchive::createDirectory(const FSPath& path) {
std::filesystem::path p = IOFile::getAppData() / "SDMC";
switch (path.type) {
case PathType::ASCII:
if (!isPathSafe<PathType::ASCII>(path)) {
Helpers::panic("Unsafe path in SDMCArchive::OpenFile");
}
p += fs::path(path.string).make_preferred();
break;
case PathType::UTF16:
if (!isPathSafe<PathType::UTF16>(path)) {
Helpers::panic("Unsafe path in SDMCArchive::OpenFile");
}
p += fs::path(path.utf16_string).make_preferred();
break;
default: Helpers::panic("SDMCArchive::CreateDirectory: Failed. Path type: %d", path.type); return Result::FailurePlaceholder;
}
if (fs::is_directory(p)) {
return Result::FS::AlreadyExists;
}
if (fs::is_regular_file(p)) {
Helpers::panic("File path passed to SDMCArchive::CreateDirectory");
}
std::error_code ec;
bool success = fs::create_directory(p, ec);
return success ? Result::Success : Result::FS::UnexpectedFileOrDir;
}
Rust::Result<DirectorySession, HorizonResult> SDMCArchive::openDirectory(const FSPath& path) {
if (path.type == PathType::UTF16) {
if (!isPathSafe<PathType::UTF16>(path)) {
Helpers::panic("Unsafe path in SaveData::OpenDirectory");
}
fs::path p = IOFile::getAppData() / "SDMC";
p += fs::path(path.utf16_string).make_preferred();
if (fs::is_regular_file(p)) {
printf("SDMC: OpenDirectory used with a file path");
return Err(Result::FS::UnexpectedFileOrDir);
}
if (fs::is_directory(p)) {
return Ok(DirectorySession(this, p));
} else {
return Err(Result::FS::FileNotFoundAlt);
}
}
Helpers::panic("SDMCArchive::OpenDirectory: Unimplemented path type");
return Err(Result::Success);
}
Rust::Result<ArchiveBase*, HorizonResult> SDMCArchive::openArchive(const FSPath& path) {
printf("SDMCArchive::OpenArchive: Failed\n");
return Err(Result::FS::NotFormatted);
// TODO: Fail here if the SD is disabled in the connfig.
if (path.type != PathType::Empty) {
Helpers::panic("Unimplemented path type for SDMC::OpenArchive");
}
return Ok((ArchiveBase*)this);
}
std::optional<u32> SDMCArchive::readFile(FileSession* file, u64 offset, u32 size, u32 dataPointer) {

View file

@ -69,57 +69,77 @@ std::optional<u32> SelfNCCHArchive::readFile(FileSession* file, u64 offset, u32
return std::nullopt;
}
auto cxi = mem.getCXI();
IOFile& ioFile = mem.CXIFile;
bool success = false;
std::size_t bytesRead = 0;
std::unique_ptr<u8[]> data(new u8[size]);
NCCH::FSInfo fsInfo;
if (auto cxi = mem.getCXI(); cxi != nullptr) {
IOFile& ioFile = mem.CXIFile;
// Seek to file offset depending on if we're reading from RomFS, ExeFS, etc
switch (type) {
case PathType::RomFS: {
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");
NCCH::FSInfo fsInfo;
// Seek to file offset depending on if we're reading from RomFS, ExeFS, etc
switch (type) {
case PathType::RomFS: {
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;
}
fsInfo = cxi->romFS;
offset += 0x1000;
break;
}
case PathType::ExeFS: {
const u64 exeFSSize = cxi->exeFS.size;
const u64 exeFSOffset = cxi->exeFS.offset;
if ((offset >> 32) || (offset >= exeFSSize) || (offset + size >= exeFSSize)) {
Helpers::panic("Tried to read from SelfNCCH with too big of an offset");
}
case PathType::ExeFS: {
const u64 exeFSSize = cxi->exeFS.size;
const u64 exeFSOffset = cxi->exeFS.offset;
if ((offset >> 32) || (offset >= exeFSSize) || (offset + size >= exeFSSize)) {
Helpers::panic("Tried to read from SelfNCCH with too big of an offset");
fsInfo = cxi->exeFS;
break;
}
fsInfo = cxi->exeFS;
break;
}
// 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");
// 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");
}
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;
}
fsInfo = cxi->romFS;
offset += 0x1000;
break;
default: Helpers::panic("Unimplemented file path type for SelfNCCH archive");
}
default: Helpers::panic("Unimplemented file path type for SelfNCCH archive");
std::tie(success, bytesRead) = cxi->readFromFile(ioFile, fsInfo, &data[0], offset, size);
}
std::unique_ptr<u8[]> data(new u8[size]);
auto [success, bytesRead] = cxi->readFromFile(ioFile, fsInfo, &data[0], offset, size);
else if (auto hb3dsx = mem.get3DSX(); hb3dsx != nullptr) {
switch (type) {
case PathType::RomFS: {
const u64 romFSSize = hb3dsx->romFSSize;
if ((offset >> 32) || (offset >= romFSSize) || (offset + size >= romFSSize)) {
Helpers::panic("Tried to read from SelfNCCH with too big of an offset");
}
break;
}
default: Helpers::panic("Unimplemented file path type for 3DSX SelfNCCH archive");
}
std::tie(success, bytesRead) = hb3dsx->readRomFSBytes(&data[0], offset, size);
}
if (!success) {
Helpers::panic("Failed to read from SelfNCCH archive");

View file

@ -124,6 +124,7 @@ void Kernel::deleteObjectData(KernelObject& object) {
case KernelObjectType::Session: delete object.getData<Session>(); return;
case KernelObjectType::Mutex: delete object.getData<Mutex>(); return;
case KernelObjectType::Semaphore: delete object.getData<Semaphore>(); return;
case KernelObjectType::Timer: delete object.getData<Timer>(); return;
case KernelObjectType::Thread: return;
case KernelObjectType::Dummy: return;
default: [[unlikely]] Helpers::warn("unknown object type"); return;
@ -262,6 +263,8 @@ void Kernel::duplicateHandle() {
}
}
void Kernel::clearInstructionCache() { cpu.clearCache(); }
namespace SystemInfoType {
enum : u32 {
MemoryInformation = 0,

View file

@ -252,6 +252,14 @@ void Kernel::acquireSyncObject(KernelObject* object, const Thread& thread) {
case KernelObjectType::Thread:
break;
case KernelObjectType::Timer: {
Timer* timer = object->getData<Timer>();
if (timer->resetType == ResetType::OneShot) { // One-shot timers automatically get cleared after waking up a thread
timer->fired = false;
}
break;
}
default: Helpers::panic("Acquiring unimplemented sync object %s", object->getTypeName());
}
}
@ -652,6 +660,9 @@ bool Kernel::shouldWaitOnObject(KernelObject* object) {
case KernelObjectType::Thread: // Waiting on a thread waits until it's dead. If it's dead then no need to wait
return object->getData<Thread>()->status != ThreadStatus::Dead;
case KernelObjectType::Timer: // We should wait on a timer only if it has not been signalled
return !object->getData<Timer>()->fired;
case KernelObjectType::Semaphore: // Wait if the semaphore count <= 0
return object->getData<Semaphore>()->availableCount <= 0;

View file

@ -1,6 +1,127 @@
#include "kernel.hpp"
#include "cpu.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"); }
Handle Kernel::makeTimer(ResetType type) {
Handle ret = makeObject(KernelObjectType::Timer);
objects[ret].data = new Timer(type);
if (type == ResetType::Pulse) {
Helpers::panic("Created pulse timer");
}
// timerHandles.push_back(ret);
return ret;
}
void Kernel::updateTimer(Handle handle, Timer* timer) {
if (timer->running) {
const u64 currentTicks = cpu.getTicks();
u64 elapsedTicks = currentTicks - timer->startTick;
constexpr double ticksPerSec = double(CPU::ticksPerSec);
constexpr double nsPerTick = ticksPerSec / 1000000000.0;
const s64 elapsedNs = s64(double(elapsedTicks) * nsPerTick);
// Timer has fired
if (elapsedNs >= timer->currentDelay) {
timer->startTick = currentTicks;
timer->currentDelay = timer->interval;
signalTimer(handle, timer);
}
}
}
void Kernel::cancelTimer(Timer* timer) {
timer->running = false;
// TODO: When we have a scheduler this should properly cancel timer events in the scheduler
}
void Kernel::signalTimer(Handle timerHandle, Timer* timer) {
timer->fired = true;
requireReschedule();
// Check if there's any thread waiting on this event
if (timer->waitlist != 0) {
wakeupAllThreads(timer->waitlist, timerHandle);
timer->waitlist = 0; // No threads waiting;
switch (timer->resetType) {
case ResetType::OneShot: timer->fired = false; break;
case ResetType::Sticky: break;
case ResetType::Pulse: Helpers::panic("Signalled pulsing timer"); break;
}
}
}
void Kernel::svcCreateTimer() {
const u32 resetType = regs[1];
if (resetType > 2) {
Helpers::panic("Invalid reset type for event %d", resetType);
}
// Have a warning here until our timers don't suck
Helpers::warn("Called Kernel::CreateTimer. Timers are currently not updated nor triggered properly!");
logSVC("CreateTimer (resetType = %s)\n", resetTypeToString(resetType));
regs[0] = Result::Success;
regs[1] = makeTimer(static_cast<ResetType>(resetType));
}
void Kernel::svcSetTimer() {
Handle handle = regs[0];
// TODO: Is this actually s64 or u64? 3DBrew says s64, but u64 makes more sense
const s64 initial = s64(u64(regs[1]) | (u64(regs[2]) << 32));
const s64 interval = s64(u64(regs[3]) | (u64(regs[4]) << 32));
logSVC("SetTimer (handle = %X, initial delay = %llX, interval delay = %llX)\n", handle, initial, interval);
KernelObject* object = getObject(handle, KernelObjectType::Timer);
if (object == nullptr) {
Helpers::panic("Tried to set non-existent timer %X\n", handle);
regs[0] = Result::Kernel::InvalidHandle;
}
Timer* timer = object->getData<Timer>();
cancelTimer(timer);
timer->currentDelay = initial;
timer->interval = interval;
timer->running = true;
timer->startTick = cpu.getTicks();
// If the initial delay is 0 then instantly signal the timer
if (initial == 0) {
signalTimer(handle, timer);
} else {
// This should schedule an event in the scheduler when we have one
}
regs[0] = Result::Success;
}
void Kernel::svcClearTimer() {
Handle handle = regs[0];
logSVC("ClearTimer (handle = %X)\n", handle);
KernelObject* object = getObject(handle, KernelObjectType::Timer);
if (object == nullptr) {
Helpers::panic("Tried to clear non-existent timer %X\n", handle);
regs[0] = Result::Kernel::InvalidHandle;
} else {
object->getData<Timer>()->fired = false;
regs[0] = Result::Success;
}
}
void Kernel::svcCancelTimer() {
Handle handle = regs[0];
logSVC("CancelTimer (handle = %X)\n", handle);
KernelObject* object = getObject(handle, KernelObjectType::Timer);
if (object == nullptr) {
Helpers::panic("Tried to cancel non-existent timer %X\n", handle);
regs[0] = Result::Kernel::InvalidHandle;
} else {
cancelTimer(object->getData<Timer>());
regs[0] = Result::Success;
}
}

302
src/core/loader/3dsx.cpp Normal file
View file

@ -0,0 +1,302 @@
#include "loader/3dsx.hpp"
#include <cstring>
#include <optional>
#include <span>
#include "memory.hpp"
namespace {
struct LoadInfo {
u32 codeSegSizeAligned;
u32 rodataSegSizeAligned;
u32 dataSegSizeAligned;
};
static inline u32 translateAddr(const u32 off, const u32* addrs, const u32* offsets) {
if (off < offsets[1]) {
return addrs[0] + off;
}
if (off < offsets[2]) {
return addrs[1] + off - offsets[1];
}
return addrs[2] + off - offsets[2];
}
} // namespace
bool Memory::map3DSX(HB3DSX& hb3dsx, const HB3DSX::Header& header) {
const LoadInfo hbInfo = {
.codeSegSizeAligned = (header.codeSegSize + 0xFFF) & ~0xFFF,
.rodataSegSizeAligned = (header.rodataSegSize + 0xFFF) & ~0xFFF,
.dataSegSizeAligned = (header.dataSegSize + 0xFFF) & ~0xFFF,
};
const u32 textSegAddr = HB3DSX::entrypoint;
const u32 rodataSegAddr = textSegAddr + hbInfo.codeSegSizeAligned;
const u32 dataSegAddr = rodataSegAddr + hbInfo.rodataSegSizeAligned;
const u32 extraPageAddr = dataSegAddr + hbInfo.dataSegSizeAligned;
printf("Text address = %08X, size = %08X\n", textSegAddr, hbInfo.codeSegSizeAligned);
printf("Rodata address = %08X, size = %08X\n", rodataSegAddr, hbInfo.rodataSegSizeAligned);
printf("Data address = %08X, size = %08X\n", dataSegAddr, hbInfo.dataSegSizeAligned);
// Allocate stack, 3dsx/libctru don't require anymore than this
if (!allocateMainThreadStack(4_KB)) {
// Should be unreachable
printf("Failed to allocate stack for 3DSX.\n");
return false;
}
// Map code file to memory
// Total memory to allocate for loading
// suum of aligned values is always aligned, have an extra RW page for libctru
const u32 totalSize = hbInfo.codeSegSizeAligned + hbInfo.rodataSegSizeAligned + hbInfo.dataSegSizeAligned + 4_KB;
const auto opt = findPaddr(totalSize);
if (!opt.has_value()) {
Helpers::panic("Failed to find paddr to map 3DSX file's code to");
return false;
}
// Map the ROM on the kernel side
const u32 textOffset = 0;
const u32 rodataOffset = textOffset + hbInfo.codeSegSizeAligned;
const u32 dataOffset = rodataOffset + hbInfo.rodataSegSizeAligned;
const u32 extraPageOffset = dataOffset + hbInfo.dataSegSizeAligned;
std::array<HB3DSX::RelocHeader, 3> relocHeaders;
auto [success, count] = hb3dsx.file.read(&relocHeaders[0], relocHeaders.size(), sizeof(HB3DSX::RelocHeader));
if (!success || count != relocHeaders.size()) {
Helpers::panic("Failed to read 3DSX relocation headers");
return false;
}
const u32 dataLoadsize = header.dataSegSize - header.bssSize; // 3DSX data size in header includes bss
std::vector<u8> code(totalSize, 0);
std::tie(success, count) = hb3dsx.file.readBytes(&code[textOffset], header.codeSegSize);
if (!success || count != header.codeSegSize) {
Helpers::panic("Failed to read 3DSX text segment");
return false;
}
std::tie(success, count) = hb3dsx.file.readBytes(&code[rodataOffset], header.rodataSegSize);
if (!success || count != header.rodataSegSize) {
Helpers::panic("Failed to read 3DSX rodata segment");
return false;
}
std::tie(success, count) = hb3dsx.file.readBytes(&code[dataOffset], dataLoadsize);
if (!success || count != dataLoadsize) {
Helpers::panic("Failed to read 3DSX data segment");
return false;
}
std::vector<HB3DSX::Reloc> currentRelocs;
const u32 segAddrs[] = {
textSegAddr,
rodataSegAddr,
dataSegAddr,
extraPageAddr,
};
const u32 segOffs[] = {
textOffset,
rodataOffset,
dataOffset,
extraPageOffset,
};
const u32 segSizes[] = {
header.codeSegSize,
header.rodataSegSize,
dataLoadsize,
0x1000,
};
for (const auto& relocHeader : relocHeaders) {
currentRelocs.resize(relocHeader.absoluteCount + relocHeader.relativeCount);
std::tie(success, count) = hb3dsx.file.read(&currentRelocs[0], currentRelocs.size(), sizeof(HB3DSX::Reloc));
if (!success || count != currentRelocs.size()) {
Helpers::panic("Failed to read 3DSX relocations");
return false;
}
const auto allRelocs = std::span(currentRelocs);
const auto absoluteRelocs = allRelocs.subspan(0, relocHeader.absoluteCount);
const auto relativeRelocs = allRelocs.subspan(relocHeader.absoluteCount, relocHeader.relativeCount);
const auto currentSeg = &relocHeader - &relocHeaders[0];
const auto sectionDataStartAs = std::span(code).subspan(segOffs[currentSeg], segSizes[currentSeg]);
auto sectionData = sectionDataStartAs;
const auto RelocationAction = [&](const HB3DSX::Reloc& reloc, const HB3DSX::RelocType relocType) -> bool {
if (reloc.skip) {
sectionData = sectionData.subspan(reloc.skip * sizeof(u32)); // advance by `skip` words (32-bit values)
}
for (u32 m = 0; m < reloc.patch && !sectionData.empty(); ++m) {
const u32 inAddr = textSegAddr + (sectionData.data() - code.data()); // byte offset -> word count
u32 origData = 0;
std::memcpy(&origData, &sectionData[0], sizeof(u32));
const u32 subType = origData >> (32 - 4);
const u32 addr = translateAddr(origData & ~0xF0000000, segAddrs, segOffs);
switch (relocType) {
case HB3DSX::RelocType::Absolute: {
if (subType != 0) {
Helpers::panic("Unsupported absolute reloc subtype");
return false;
}
std::memcpy(&sectionData[0], &addr, sizeof(u32));
break;
}
case HB3DSX::RelocType::Relative: {
u32 data = addr - inAddr;
switch (subType) {
case 1: // 31-bit signed offset
data &= ~(1u << 31);
case 0: // 32-bit signed offset
std::memcpy(&sectionData[0], &data, sizeof(u32));
break;
default: Helpers::panic("Unsupported relative reloc subtype"); return false;
}
break;
}
}
sectionData = sectionData.subspan(sizeof(u32));
}
return true;
};
for (const auto& reloc : absoluteRelocs) {
if (!RelocationAction(reloc, HB3DSX::RelocType::Absolute)) {
return false;
}
}
sectionData = sectionDataStartAs; // restart from the beginning for the next part
for (const auto& reloc : relativeRelocs) {
if (!RelocationAction(reloc, HB3DSX::RelocType::Relative)) {
return false;
}
}
}
// Detect and fill _prm structure
HB3DSX::PrmStruct pst;
std::memcpy(&pst, &code[4], sizeof(pst));
if (pst.magic[0] == '_' && pst.magic[1] == 'p' && pst.magic[2] == 'r' && pst.magic[3] == 'm') {
// if there was any argv to put, it would go there
// first u32: argc
// remaining: continuous argv string (NUL-char separated, ofc)
// std::memcpy(&code[extraPageOffset], argvBuffer, ...);
// setting to NULL (default) = run from system. load romfs from process.
// non-NULL = homebrew launcher. load romfs from 3dsx @ argv[0]
// pst.pSrvOverride = extraPageAddr + 0xFFC;
pst.pArgList = extraPageAddr;
// RUNFLAG_APTREINIT: Reinitialize APT.
// From libctru. Because there's no previously running software here
pst.runFlags |= 1 << 1;
/* s64 dummy;
bool isN3DS = svcGetSystemInfo(&dummy, 0x10001, 0) == 0;
if (isN3DS)
{
pst->heapSize = u32(48_MB);
pst->linearHeapSize = u32(64_MB);
} else */ {
pst.heapSize = u32(24_MB);
pst.linearHeapSize = u32(32_MB);
}
std::memcpy(&code[4], &pst, sizeof(pst));
}
const auto paddr = opt.value();
std::memcpy(&fcram[paddr], &code[0], totalSize); // Copy the 3 segments + BSS to FCRAM
allocateMemory(textSegAddr, paddr + textOffset, hbInfo.codeSegSizeAligned, true, true, false, true); // Text is R-X
allocateMemory(rodataSegAddr, paddr + rodataOffset, hbInfo.rodataSegSizeAligned, true, true, false, false); // Rodata is R--
allocateMemory(dataSegAddr, paddr + dataOffset, hbInfo.dataSegSizeAligned + 0x1000, true, true, true, false); // Data+BSS+Extra is RW-
return true;
}
std::optional<u32> Memory::load3DSX(const std::filesystem::path& path) {
HB3DSX hb3dsx;
if (!hb3dsx.file.open(path, "rb")) {
return std::nullopt;
}
u8 magic[4]; // Must be "3DSX"
auto [success, bytes] = hb3dsx.file.readBytes(magic, 4);
if (!success || bytes != 4) {
printf("Failed to read 3DSX magic\n");
return std::nullopt;
}
if (magic[0] != '3' || magic[1] != 'D' || magic[2] != 'S' || magic[3] != 'X') {
printf("3DSX with wrong magic value\n");
return std::nullopt;
}
HB3DSX::Header hbHeader;
std::tie(success, bytes) = hb3dsx.file.readBytes(&hbHeader, sizeof(hbHeader));
if (!success || bytes != sizeof(hbHeader)) {
printf("Failed to read 3DSX header\n");
return std::nullopt;
}
if (hbHeader.headerSize == 0x20 || hbHeader.headerSize == 0x2C) {
if (hbHeader.headerSize == 0x2C) {
hb3dsx.file.seek(8, SEEK_CUR); // skip SMDH info
std::tie(success, bytes) = hb3dsx.file.readBytes(&hb3dsx.romFSOffset, 4);
if (!success || bytes != 4) {
printf("Failed to read 3DSX romFS offset\n");
return std::nullopt;
}
const auto fileSize = hb3dsx.file.size();
if (!fileSize) {
printf("Failed to get 3DSX size\n");
return std::nullopt;
}
hb3dsx.romFSSize = *fileSize - hb3dsx.romFSOffset;
}
} else {
printf("Invalid 3DSX header size\n");
return std::nullopt;
}
if (!map3DSX(hb3dsx, hbHeader)) {
printf("Failed to map 3DSX\n");
return std::nullopt;
}
loaded3DSX = std::move(hb3dsx);
return HB3DSX::entrypoint;
}
bool HB3DSX::hasRomFs() const { return romFSSize != 0 && romFSOffset != 0; }
std::pair<bool, std::size_t> HB3DSX::readRomFSBytes(void* dst, std::size_t offset, std::size_t size) {
if (!hasRomFs()) {
return {false, 0};
}
if (!file.seek(romFSOffset + offset)) {
return {false, 0};
}
return file.readBytes(dst, size);
}

View file

@ -150,6 +150,7 @@ u32 Memory::read32(u32 vaddr) {
return *(u32*)(pointer + offset);
} else {
switch (vaddr) {
case 0x1FF80000: return u32(kernelVersion) << 16;
case ConfigMem::Datetime0: return u32(timeSince3DSEpoch()); // ms elapsed since Jan 1 1900, bottom 32 bits
case ConfigMem::Datetime0 + 4:
return u32(timeSince3DSEpoch() >> 32); // top 32 bits

View file

@ -695,6 +695,7 @@ void RendererGL::textureCopy(u32 inputAddr, u32 outputAddr, u32 totalBytes, u32
if (inputGap != 0 || outputGap != 0) {
// Helpers::warn("Strided texture copy\n");
}
if (inputWidth != outputWidth) {
Helpers::warn("Input width does not match output width, cannot accelerate texture copy!");
return;
@ -716,7 +717,12 @@ void RendererGL::textureCopy(u32 inputAddr, u32 outputAddr, u32 totalBytes, u32
// inputHeight/outputHeight are typically set to zero so they cannot be used to get the height of the copy region
// in contrast to display transfer. Compute height manually by dividing the copy size with the copy width. The result
// is the number of vertical tiles so multiply that by eight to get the actual copy height.
const u32 copyHeight = (copySize / inputWidth) * 8;
u32 copyHeight;
if (inputWidth != 0) [[likely]] {
copyHeight = (copySize / inputWidth) * 8;
} else {
copyHeight = 0;
}
// Find the source surface.
auto srcFramebuffer = getColourBuffer(inputAddr, PICA::ColorFmt::RGBA8, copyStride, copyHeight, false);

View file

@ -17,7 +17,10 @@ void ACTService::handleSyncRequest(u32 messagePointer) {
case ACTCommands::GenerateUUID: generateUUID(messagePointer); break;
case ACTCommands::GetAccountDataBlock: getAccountDataBlock(messagePointer); break;
case ACTCommands::Initialize: initialize(messagePointer); break;
default: Helpers::panic("ACT service requested. Command: %08X\n", command);
default:
Helpers::warn("Undocumented ACT service requested. Command: %08X", command);
mem.write32(messagePointer + 4, Result::Success);
break;
}
}

View file

@ -6,7 +6,10 @@ namespace CAMCommands {
enum : u32 {
GetBufferErrorInterruptEvent = 0x00060040,
DriverInitialize = 0x00390000,
SetTransferLines = 0x00090100,
GetMaxLines = 0x000A0080,
SetFrameRate = 0x00200080,
SetContrast = 0x00230080,
};
}
@ -18,7 +21,12 @@ void CAMService::handleSyncRequest(u32 messagePointer) {
case CAMCommands::DriverInitialize: driverInitialize(messagePointer); break;
case CAMCommands::GetBufferErrorInterruptEvent: getBufferErrorInterruptEvent(messagePointer); break;
case CAMCommands::GetMaxLines: getMaxLines(messagePointer); break;
default: Helpers::panic("CAM service requested. Command: %08X\n", command);
case CAMCommands::SetContrast: setContrast(messagePointer); break;
case CAMCommands::SetFrameRate: setFrameRate(messagePointer); break;
case CAMCommands::SetTransferLines: setTransferLines(messagePointer); break;
default:
Helpers::panic("Unimplemented CAM service requested. Command: %08X\n", command);
break;
}
}
@ -28,6 +36,38 @@ void CAMService::driverInitialize(u32 messagePointer) {
mem.write32(messagePointer + 4, Result::Success);
}
void CAMService::setContrast(u32 messagePointer) {
const u32 cameraSelect = mem.read32(messagePointer + 4);
const u32 contrast = mem.read32(messagePointer + 8);
log("CAM::SetPhotoMode (camera select = %d, contrast = %d)\n", cameraSelect, contrast);
mem.write32(messagePointer, IPC::responseHeader(0x23, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void CAMService::setTransferLines(u32 messagePointer) {
const u32 port = mem.read32(messagePointer + 4);
const s16 lines = mem.read16(messagePointer + 8);
const s16 width = mem.read16(messagePointer + 12);
const s16 height = mem.read16(messagePointer + 16);
log("CAM::SetTransferLines (port = %d, lines = %d, width = %d, height = %d)\n", port, lines, width, height);
mem.write32(messagePointer, IPC::responseHeader(0x9, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void CAMService::setFrameRate(u32 messagePointer) {
const u32 cameraSelect = mem.read32(messagePointer + 4);
const u32 framerate = mem.read32(messagePointer + 8);
log("CAM::SetPhotoMode (camera select = %d, framerate = %d)\n", cameraSelect, framerate);
mem.write32(messagePointer, IPC::responseHeader(0x20, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
// Algorithm taken from Citra
// https://github.com/citra-emu/citra/blob/master/src/core/hle/service/cam/cam.cpp#L465
void CAMService::getMaxLines(u32 messagePointer) {

View file

@ -2,6 +2,7 @@
#include "kernel/kernel.hpp"
#include "io_file.hpp"
#include "ipc.hpp"
#include "result/result.hpp"
#ifdef CreateFile // windows.h defines CreateFile & DeleteFile because of course it does.
#undef CreateDirectory
@ -25,13 +26,17 @@ namespace FSCommands {
GetFreeBytes = 0x08120080,
IsSdmcDetected = 0x08170000,
IsSdmcWritable = 0x08180000,
AbnegateAccessRight = 0x08400040,
GetFormatInfo = 0x084500C2,
GetArchiveResource = 0x08490040,
FormatSaveData = 0x084C0242,
CreateExtSaveData = 0x08510242,
DeleteExtSaveData = 0x08520100,
SetArchivePriority = 0x085A00C0,
InitializeWithSdkVersion = 0x08610042,
SetPriority = 0x08620040,
GetPriority = 0x08630000,
SetThisSaveDataSecureValue = 0x086E00C0,
GetThisSaveDataSecureValue = 0x086F0040,
TheGameboyVCFunction = 0x08750180,
};
@ -158,6 +163,7 @@ void FSService::handleSyncRequest(u32 messagePointer) {
case FSCommands::DeleteFile: deleteFile(messagePointer); break;
case FSCommands::FormatSaveData: formatSaveData(messagePointer); break;
case FSCommands::FormatThisUserSaveData: formatThisUserSaveData(messagePointer); break;
case FSCommands::GetArchiveResource: getArchiveResource(messagePointer); break;
case FSCommands::GetFreeBytes: getFreeBytes(messagePointer); break;
case FSCommands::GetFormatInfo: getFormatInfo(messagePointer); break;
case FSCommands::GetPriority: getPriority(messagePointer); break;
@ -170,7 +176,10 @@ void FSService::handleSyncRequest(u32 messagePointer) {
case FSCommands::OpenDirectory: openDirectory(messagePointer); break;
case FSCommands::OpenFile: [[likely]] openFile(messagePointer); break;
case FSCommands::OpenFileDirectly: [[likely]] openFileDirectly(messagePointer); break;
case FSCommands::SetArchivePriority: setArchivePriority(messagePointer); break;
case FSCommands::SetPriority: setPriority(messagePointer); break;
case FSCommands::SetThisSaveDataSecureValue: setThisSaveDataSecureValue(messagePointer); break;
case FSCommands::AbnegateAccessRight: abnegateAccessRight(messagePointer); break;
case FSCommands::TheGameboyVCFunction: theGameboyVCFunction(messagePointer); break;
default: Helpers::panic("FS service requested. Command: %08X\n", command);
}
@ -344,7 +353,8 @@ void FSService::openFileDirectly(u32 messagePointer) {
std::optional<Handle> handle = openFileHandle(archive, filePath, archivePath, perms);
mem.write32(messagePointer, IPC::responseHeader(0x803, 1, 2));
if (!handle.has_value()) {
Helpers::panic("OpenFileDirectly: Failed to open file with given path");
printf("OpenFileDirectly failed\n");
mem.write32(messagePointer + 4, Result::FS::FileNotFound);
} else {
mem.write32(messagePointer + 4, Result::Success);
mem.write32(messagePointer + 12, handle.value());
@ -579,6 +589,36 @@ void FSService::getPriority(u32 messagePointer) {
mem.write32(messagePointer + 8, priority);
}
void FSService::getArchiveResource(u32 messagePointer) {
const u32 mediaType = mem.read32(messagePointer + 4);
log("FS::GetArchiveResource (media type = %d) (stubbed)\n");
// For the time being, return the same stubbed archive resource for every media type
static constexpr ArchiveResource resource = {
.sectorSize = 512,
.clusterSize = 16_KB,
.partitionCapacityInClusters = 0x80000, // 0x80000 * 16 KB = 8GB
.freeSpaceInClusters = 0x80000, // Same here
};
mem.write32(messagePointer, IPC::responseHeader(0x849, 5, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write32(messagePointer + 8, resource.sectorSize);
mem.write32(messagePointer + 12, resource.clusterSize);
mem.write32(messagePointer + 16, resource.partitionCapacityInClusters);
mem.write32(messagePointer + 20, resource.freeSpaceInClusters);
}
void FSService::setArchivePriority(u32 messagePointer) {
Handle archive = mem.read64(messagePointer + 4);
const u32 value = mem.read32(messagePointer + 12);
log("FS::SetArchivePriority (priority = %d, archive handle = %X)\n", value, handle);
mem.write32(messagePointer, IPC::responseHeader(0x85A, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void FSService::setPriority(u32 messagePointer) {
const u32 value = mem.read32(messagePointer + 4);
log("FS::SetPriority (priority = %d)\n", value);
@ -588,6 +628,18 @@ void FSService::setPriority(u32 messagePointer) {
priority = value;
}
void FSService::abnegateAccessRight(u32 messagePointer) {
const u32 right = mem.read32(messagePointer + 4);
log("FS::AbnegateAccessRight (right = %d)\n", right);
if (right >= 0x38) {
Helpers::warn("FS::AbnegateAccessRight: Invalid access right");
}
mem.write32(messagePointer, IPC::responseHeader(0x840, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void FSService::getThisSaveDataSecureValue(u32 messagePointer) {
Helpers::warn("Unimplemented FS::GetThisSaveDataSecureValue");
@ -595,6 +647,19 @@ void FSService::getThisSaveDataSecureValue(u32 messagePointer) {
mem.write32(messagePointer + 4, Result::Success);
}
void FSService::setThisSaveDataSecureValue(u32 messagePointer) {
const u64 value = mem.read32(messagePointer + 4);
const u32 slot = mem.read32(messagePointer + 12);
const u32 id = mem.read32(messagePointer + 16);
const u8 variation = mem.read8(messagePointer + 20);
// TODO: Actually do something with this.
Helpers::warn("Unimplemented FS::SetThisSaveDataSecureValue");
mem.write32(messagePointer, IPC::responseHeader(0x86E, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void FSService::theGameboyVCFunction(u32 messagePointer) {
Helpers::warn("Unimplemented FS: function: 0x08750180");
@ -602,21 +667,21 @@ void FSService::theGameboyVCFunction(u32 messagePointer) {
mem.write32(messagePointer + 4, Result::Success);
}
// Shows whether an SD card is inserted. At the moment stubbed to no
constexpr bool sdInserted = false;
void FSService::isSdmcDetected(u32 messagePointer) {
log("FS::IsSdmcDetected\n");
mem.write32(messagePointer, IPC::responseHeader(0x817, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write8(messagePointer + 8, sdInserted ? 1 : 0);
mem.write8(messagePointer + 8, config.sdCardInserted ? 1 : 0);
}
// We consider our SD card to always be writable if oen is inserted for now
// So isSdmcWritable returns 1 if an SD card is inserted (because it's always writable) and 0 if not.
// We consider our SD card to always be writable if one is inserted for now
// However we do make sure to respect the configs and properly return the correct value here
void FSService::isSdmcWritable(u32 messagePointer) {
log("FS::isSdmcWritable\n");
const bool writeProtected = (!config.sdCardInserted) || (config.sdCardInserted && config.sdWriteProtected);
mem.write32(messagePointer, IPC::responseHeader(0x818, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write8(messagePointer + 8, sdInserted ? 1 : 0);
mem.write8(messagePointer + 8, writeProtected ? 0 : 1);
}

View file

@ -6,6 +6,8 @@
namespace HTTPCommands {
enum : u32 {
Initialize = 0x00010044,
CreateRootCertChain = 0x002D0000,
RootCertChainAddDefaultCert = 0x00300080,
};
}
@ -14,7 +16,9 @@ void HTTPService::reset() { initialized = false; }
void HTTPService::handleSyncRequest(u32 messagePointer) {
const u32 command = mem.read32(messagePointer);
switch (command) {
case HTTPCommands::CreateRootCertChain: createRootCertChain(messagePointer); break;
case HTTPCommands::Initialize: initialize(messagePointer); break;
case HTTPCommands::RootCertChainAddDefaultCert: rootCertChainAddDefaultCert(messagePointer); break;
default: Helpers::panic("HTTP service requested. Command: %08X\n", command);
}
}
@ -39,4 +43,28 @@ void HTTPService::initialize(u32 messagePointer) {
initialized = true;
// We currently don't emulate HTTP properly. TODO: Prepare POST buffer here
mem.write32(messagePointer + 4, Result::Success);
}
void HTTPService::createRootCertChain(u32 messagePointer) {
log("HTTP::CreateRootCertChain (Unimplemented)\n");
// TODO: Verify response header
mem.write32(messagePointer, IPC::responseHeader(0x2D, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
// RootCertChain context handle. No need to emulate this yet
mem.write32(messagePointer + 8, 0x66666666);
}
void HTTPService::rootCertChainAddDefaultCert(u32 messagePointer) {
log("HTTP::RootCertChainAddDefaultCert (Unimplemented)\n");
const u32 contextHandle = mem.read32(messagePointer + 4);
const u32 certID = mem.read32(messagePointer + 8);
// TODO: Verify response header
mem.write32(messagePointer, IPC::responseHeader(0x30, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
// Cert context handle. No need to emulate this yet
mem.write32(messagePointer + 8, 0x66666666);
}

View file

@ -13,6 +13,7 @@ namespace MICCommands {
SetGain = 0x00080040,
GetGain = 0x00090000,
SetPower = 0x000A0040,
GetPower = 0x000B0000,
SetIirFilter = 0x000C0042,
SetClamp = 0x000D0040,
CaptainToadFunction = 0x00100040,
@ -33,6 +34,7 @@ void MICService::handleSyncRequest(u32 messagePointer) {
switch (command) {
case MICCommands::GetEventHandle: getEventHandle(messagePointer); break;
case MICCommands::GetGain: getGain(messagePointer); break;
case MICCommands::GetPower: getPower(messagePointer); break;
case MICCommands::IsSampling: isSampling(messagePointer); break;
case MICCommands::MapSharedMem: mapSharedMem(messagePointer); break;
case MICCommands::SetClamp: setClamp(messagePointer); break;
@ -101,6 +103,14 @@ void MICService::setPower(u32 messagePointer) {
mem.write32(messagePointer + 4, Result::Success);
}
void MICService::getPower(u32 messagePointer) {
log("MIC::GetPower\n");
mem.write32(messagePointer, IPC::responseHeader(0xB, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write8(messagePointer + 8, micEnabled ? 1 : 0);
}
void MICService::setClamp(u32 messagePointer) {
u8 val = mem.read8(messagePointer + 4);
log("MIC::SetClamp (value = %d)\n", val);

View file

@ -5,6 +5,7 @@ namespace PTMCommands {
enum : u32 {
GetAdapterState = 0x00050000,
GetBatteryLevel = 0x00070000,
GetBatteryChargeState = 0x00080000,
GetStepHistory = 0x000B00C2,
GetTotalStepCount = 0x000C0000,
ConfigureNew3DSCPU = 0x08180040,
@ -18,6 +19,7 @@ void PTMService::handleSyncRequest(u32 messagePointer) {
switch (command) {
case PTMCommands::ConfigureNew3DSCPU: configureNew3DSCPU(messagePointer); break;
case PTMCommands::GetAdapterState: getAdapterState(messagePointer); break;
case PTMCommands::GetBatteryChargeState: getBatteryChargeState(messagePointer); break;
case PTMCommands::GetBatteryLevel: getBatteryLevel(messagePointer); break;
case PTMCommands::GetStepHistory: getStepHistory(messagePointer); break;
case PTMCommands::GetTotalStepCount: getTotalStepCount(messagePointer); break;
@ -33,6 +35,16 @@ void PTMService::getAdapterState(u32 messagePointer) {
mem.write8(messagePointer + 8, config.chargerPlugged ? 1 : 0);
}
void PTMService::getBatteryChargeState(u32 messagePointer) {
log("PTM::GetBatteryChargeState");
// We're only charging if the battery is not already full
const bool charging = config.chargerPlugged && (config.batteryPercentage < 100);
mem.write32(messagePointer, IPC::responseHeader(0x7, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write8(messagePointer + 8, charging ? 1 : 0);
}
void PTMService::getBatteryLevel(u32 messagePointer) {
log("PTM::GetBatteryLevel");

View file

@ -7,7 +7,7 @@
ServiceManager::ServiceManager(std::span<u32, 16> regs, Memory& mem, GPU& gpu, u32& currentPID, Kernel& kernel, const EmulatorConfig& config)
: regs(regs), mem(mem), kernel(kernel), ac(mem), am(mem), boss(mem), act(mem), apt(mem, kernel), cam(mem, kernel), cecd(mem, kernel), cfg(mem),
dlp_srvr(mem), dsp(mem, kernel), hid(mem, kernel), http(mem), ir_user(mem, kernel), frd(mem), fs(mem, kernel),
dlp_srvr(mem), dsp(mem, kernel), hid(mem, kernel), http(mem), ir_user(mem, kernel), frd(mem), fs(mem, kernel, config),
gsp_gpu(mem, gpu, kernel, currentPID), gsp_lcd(mem), ldr(mem), mcu_hwc(mem, config), mic(mem, kernel), nfc(mem, kernel), nim(mem), ndm(mem),
news_u(mem), ptm(mem, config), soc(mem), ssl(mem), y2r(mem, kernel) {}
@ -79,6 +79,7 @@ void ServiceManager::handleSyncRequest(u32 messagePointer) {
case Commands::RegisterClient: registerClient(messagePointer); break;
case Commands::GetServiceHandle: getServiceHandle(messagePointer); break;
case Commands::Subscribe: subscribe(messagePointer); break;
case Commands::Unsubscribe: unsubscribe(messagePointer); break;
default: Helpers::panic("Unknown \"srv:\" command: %08X", header);
}
}
@ -178,6 +179,14 @@ void ServiceManager::subscribe(u32 messagePointer) {
mem.write32(messagePointer + 4, Result::Success);
}
void ServiceManager::unsubscribe(u32 messagePointer) {
u32 id = mem.read32(messagePointer + 4);
log("srv::Unsubscribe (id = %d) (stubbed)\n", id);
mem.write32(messagePointer, IPC::responseHeader(0xA, 1, 0));
mem.write32(messagePointer + 4, Result::Success);
}
void ServiceManager::sendCommandToService(u32 messagePointer, Handle handle) {
switch (handle) {
// Breaking alphabetical order a bit to place the ones I think are most common at the top

View file

@ -1,4 +1,5 @@
#include "services/y2r.hpp"
#include "ipc.hpp"
#include "kernel.hpp"
@ -6,8 +7,10 @@ namespace Y2RCommands {
enum : u32 {
SetInputFormat = 0x00010040,
SetOutputFormat = 0x00030040,
GetOutputFormat = 0x00040000,
SetRotation = 0x00050040,
SetBlockAlignment = 0x00070040,
GetBlockAlignment = 0x00080000,
SetSpacialDithering = 0x00090040,
SetTemporalDithering = 0x000B0040,
SetTransferEndInterrupt = 0x000D0040,
@ -17,7 +20,9 @@ namespace Y2RCommands {
SetSendingV = 0x00120102,
SetReceiving = 0x00180102,
SetInputLineWidth = 0x001A0040,
GetInputLineWidth = 0x001B0000,
SetInputLines = 0x001C0040,
GetInputLines = 0x001D0000,
SetStandardCoeff = 0x00200040,
SetAlpha = 0x00220040,
StartConversion = 0x00260000,
@ -26,7 +31,7 @@ namespace Y2RCommands {
SetPackageParameter = 0x002901C0,
PingProcess = 0x002A0000,
DriverInitialize = 0x002B0000,
DriverFinalize = 0x002C0000
DriverFinalize = 0x002C0000,
};
}
@ -52,6 +57,10 @@ void Y2RService::handleSyncRequest(u32 messagePointer) {
switch (command) {
case Y2RCommands::DriverInitialize: driverInitialize(messagePointer); break;
case Y2RCommands::DriverFinalize: driverFinalize(messagePointer); break;
case Y2RCommands::GetBlockAlignment: getBlockAlignment(messagePointer); break;
case Y2RCommands::GetInputLines: getInputLines(messagePointer); break;
case Y2RCommands::GetInputLineWidth: getInputLineWidth(messagePointer); break;
case Y2RCommands::GetOutputFormat: getOutputFormat(messagePointer); break;
case Y2RCommands::GetTransferEndEvent: getTransferEndEvent(messagePointer); break;
case Y2RCommands::IsBusyConversion: isBusyConversion(messagePointer); break;
case Y2RCommands::PingProcess: pingProcess(messagePointer); break;
@ -81,7 +90,7 @@ void Y2RService::pingProcess(u32 messagePointer) {
log("Y2R::PingProcess\n");
mem.write32(messagePointer, IPC::responseHeader(0x2A, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write32(messagePointer + 8, 0); // Connected number
mem.write32(messagePointer + 8, 0); // Connected number
}
void Y2RService::driverInitialize(u32 messagePointer) {
@ -98,8 +107,9 @@ void Y2RService::driverFinalize(u32 messagePointer) {
void Y2RService::getTransferEndEvent(u32 messagePointer) {
log("Y2R::GetTransferEndEvent\n");
if (!transferEndEvent.has_value())
if (!transferEndEvent.has_value()) {
transferEndEvent = kernel.makeEvent(ResetType::OneShot);
}
mem.write32(messagePointer, IPC::responseHeader(0xF, 1, 2));
mem.write32(messagePointer + 4, Result::Success);
@ -150,6 +160,14 @@ void Y2RService::setBlockAlignment(u32 messagePointer) {
mem.write32(messagePointer + 4, Result::Success);
}
void Y2RService::getBlockAlignment(u32 messagePointer) {
log("Y2R::GetBlockAlignment\n");
mem.write32(messagePointer, IPC::responseHeader(0x8, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write32(messagePointer + 8, static_cast<u32>(alignment));
}
void Y2RService::setInputFormat(u32 messagePointer) {
const u32 format = mem.read32(messagePointer + 4);
log("Y2R::SetInputFormat (format = %d)\n", format);
@ -178,6 +196,14 @@ void Y2RService::setOutputFormat(u32 messagePointer) {
mem.write32(messagePointer + 4, Result::Success);
}
void Y2RService::getOutputFormat(u32 messagePointer) {
log("Y2R::GetOutputFormat\n");
mem.write32(messagePointer, IPC::responseHeader(0x4, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write32(messagePointer + 8, static_cast<u32>(outputFmt));
}
void Y2RService::setPackageParameter(u32 messagePointer) {
// Package parameter is 3 words
const u32 word1 = mem.read32(messagePointer + 4);
@ -243,6 +269,13 @@ void Y2RService::setInputLineWidth(u32 messagePointer) {
}
}
void Y2RService::getInputLineWidth(u32 messagePointer) {
log("Y2R::GetInputLineWidth\n");
mem.write32(messagePointer, IPC::responseHeader(0x1B, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write32(messagePointer + 8, inputLineWidth);
}
void Y2RService::setInputLines(u32 messagePointer) {
const u16 lines = mem.read16(messagePointer + 4);
log("Y2R::SetInputLines (lines = %d)\n", lines);
@ -253,19 +286,30 @@ void Y2RService::setInputLines(u32 messagePointer) {
Helpers::panic("Y2R: Invalid input line count");
} else {
// According to Citra, the Y2R module seems to accidentally skip setting the line # if it's 1024
if (lines != 1024)
if (lines != 1024) {
inputLines = lines;
}
mem.write32(messagePointer + 4, Result::Success);
}
}
void Y2RService::getInputLines(u32 messagePointer) {
log("Y2R::GetInputLines\n");
mem.write32(messagePointer, IPC::responseHeader(0x1D, 2, 0));
mem.write32(messagePointer + 4, Result::Success);
mem.write32(messagePointer + 8, inputLines);
}
void Y2RService::setStandardCoeff(u32 messagePointer) {
const u32 coeff = mem.read32(messagePointer + 4);
log("Y2R::SetStandardCoeff (coefficient = %d)\n", coeff);
mem.write32(messagePointer, IPC::responseHeader(0x20, 1, 0));
if (coeff > 3)
Helpers::panic("Y2R: Invalid standard coefficient");
if (coeff > 3) {
Helpers::panic("Y2R: Invalid standard coefficient (coefficient = %d)\n", coeff);
}
else {
Helpers::warn("Unimplemented: Y2R standard coefficient");
mem.write32(messagePointer + 4, Result::Success);
@ -316,4 +360,4 @@ void Y2RService::startConversion(u32 messagePointer) {
if (transferEndEvent.has_value()) {
kernel.signalEvent(transferEndEvent.value());
}
}
}

View file

@ -426,6 +426,10 @@ bool Emulator::loadROM(const std::filesystem::path& path) {
reset(ReloadOption::NoReload);
}
// Reset whatever state needs to be reset before loading a new ROM
memory.loadedCXI = std::nullopt;
memory.loaded3DSX = std::nullopt;
// Get path for saving files (AppData on Windows, /home/user/.local/share/ApplcationName on Linux, etc)
// Inside that path, we be use a game-specific folder as well. Eg if we were loading a ROM called PenguinDemo.3ds, the savedata would be in
// %APPDATA%/Alber/PenguinDemo/SaveData on Windows, and so on. We do this because games save data in their own filesystem on the cart
@ -453,6 +457,8 @@ bool Emulator::loadROM(const std::filesystem::path& path) {
success = loadNCSD(path, ROMType::NCSD);
else if (extension == ".cxi" || extension == ".app")
success = loadNCSD(path, ROMType::CXI);
else if (extension == ".3dsx")
success = load3DSX(path);
else {
printf("Unknown file type\n");
success = false;
@ -492,6 +498,19 @@ bool Emulator::loadNCSD(const std::filesystem::path& path, ROMType type) {
return true;
}
bool Emulator::load3DSX(const std::filesystem::path& path) {
std::optional<u32> entrypoint = memory.load3DSX(path);
romType = ROMType::HB_3DSX;
if (!entrypoint.has_value()) {
return false;
}
cpu.setReg(15, entrypoint.value()); // Set initial PC
return true;
}
bool Emulator::loadELF(const std::filesystem::path& path) {
loadedELF.open(path, std::ios_base::binary); // Open ROM in binary mode
romType = ROMType::ELF;

View file

@ -345,7 +345,7 @@ void main() {
if ((textureConfig & 2u) != 0u) tevSources[4] = texture(u_tex1, v_texcoord1);
if ((textureConfig & 4u) != 0u) tevSources[5] = texture(u_tex2, tex2UV);
tevSources[13] = vec4(0.0); // Previous buffer
tevSources[15] = vec4(0.0); // Previous combiner
tevSources[15] = v_colour; // Previous combiner
tevNextPreviousBuffer = v_textureEnvBufferColor;
uint textureEnvUpdateBuffer = readPicaReg(0xE0);

View file

@ -64,7 +64,8 @@ float decodeFP(uint hex, uint E, uint M) {
void main() {
gl_Position = a_coords;
v_colour = a_vertexColour;
vec4 colourAbs = abs(a_vertexColour);
v_colour = min(colourAbs, vec4(1.f));
// Flip y axis of UVs because OpenGL uses an inverted y for texture sampling compared to the PICA
v_texcoord0 = vec3(a_texcoord0.x, 1.0 - a_texcoord0.y, a_texcoord0_w);
@ -94,4 +95,4 @@ void main() {
// There's also another, always-on clipping plane based on vertex z
gl_ClipDistance[0] = -a_coords.z;
gl_ClipDistance[1] = dot(clipData, a_coords);
}
}

@ -1 +1 @@
Subproject commit daa3dedf88cad90aaf6f6ef5e26f12884b90ca13
Subproject commit 96e179465884be74987d5847d6741cdabdfe1b48