From 3b158996e1de84f7beb326c953daf0333001b800 Mon Sep 17 00:00:00 2001 From: ITotalJustice <47043333+ITotalJustice@users.noreply.github.com> Date: Mon, 25 Apr 2022 17:50:19 +0100 Subject: [PATCH 1/3] [nx] deko3d imgui backend added added: - deko3d imgui backend (thanks ftpd) - controller input support see #70 --- README.md | 1 + src/frontend/CMakeLists.txt | 39 +- src/frontend/backend/CMakeLists.txt | 6 +- .../{sdl2/backend_sdl2.hpp => backend.hpp} | 6 +- src/frontend/backend/nx/CMakeLists.txt | 16 + src/frontend/backend/nx/backend_nx.cpp | 591 ++++++++++++++++++ .../backend/nx/ftpd_imgui/imgui_deko3d.cpp | 538 ++++++++++++++++ .../backend/nx/ftpd_imgui/imgui_deko3d.h | 87 +++ .../backend/nx/ftpd_imgui/imgui_nx.cpp | 209 +++++++ src/frontend/backend/nx/ftpd_imgui/imgui_nx.h | 39 ++ src/frontend/backend/nx/icon.jpg | Bin 0 -> 1229 bytes .../backend/nx/shaders/imgui_fsh.glsl | 48 ++ .../backend/nx/shaders/imgui_vsh.glsl | 47 ++ src/frontend/backend/sdl2/CMakeLists.txt | 4 - src/frontend/backend/sdl2/backend_sdl2.cpp | 6 +- src/frontend/main.cpp | 2 + src/frontend/system.cpp | 54 +- 17 files changed, 1662 insertions(+), 31 deletions(-) rename src/frontend/backend/{sdl2/backend_sdl2.hpp => backend.hpp} (88%) create mode 100644 src/frontend/backend/nx/CMakeLists.txt create mode 100644 src/frontend/backend/nx/backend_nx.cpp create mode 100644 src/frontend/backend/nx/ftpd_imgui/imgui_deko3d.cpp create mode 100644 src/frontend/backend/nx/ftpd_imgui/imgui_deko3d.h create mode 100644 src/frontend/backend/nx/ftpd_imgui/imgui_nx.cpp create mode 100644 src/frontend/backend/nx/ftpd_imgui/imgui_nx.h create mode 100644 src/frontend/backend/nx/icon.jpg create mode 100644 src/frontend/backend/nx/shaders/imgui_fsh.glsl create mode 100644 src/frontend/backend/nx/shaders/imgui_vsh.glsl diff --git a/README.md b/README.md index 3b4df87..aba2958 100644 --- a/README.md +++ b/README.md @@ -179,3 +179,4 @@ gba emulator witten in c++23. - ocornut for imgui - ocornut for imgui_club - everyone that has contributed to the bios decomp +- [ftpd](https://github.com/mtheall/ftpd) and [nxshell](https://github.com/joel16/NX-Shell) for the deko3d backend for switch. diff --git a/src/frontend/CMakeLists.txt b/src/frontend/CMakeLists.txt index 3cc6418..28511bd 100644 --- a/src/frontend/CMakeLists.txt +++ b/src/frontend/CMakeLists.txt @@ -82,8 +82,13 @@ else() target_link_libraries(main PRIVATE ${minizip_lib}) target_include_directories(main PRIVATE ${minizip_inc}) + # on switch, i need to manually link against + # zlib as well, and has to be linked after minizip! + find_package(ZLIB REQUIRED) + target_link_libraries(main PRIVATE ZLIB::ZLIB) + set(FOUND_MINIZIP TRUE) - message(STATUS "using system minizip") + message(STATUS "using system minizip lib: ${minizip_lib} inc: ${minizip_inc}") endif() endif() @@ -127,3 +132,35 @@ target_compile_definitions(main PRIVATE DUMP_AUDIO=$ DEBUGGER=$ ) + +if (NINTENDO_SWITCH) + # create romfs folder + dkp_add_asset_target(main_romfs ${CMAKE_CURRENT_BINARY_DIR}/romfs) + + # setup nacp + nx_generate_nacp(main.nacp + NAME "Notorious BEEG" + AUTHOR TotalJustice + VERSION 0.0.3 + ) + + # create nro (final binary) + nx_create_nro(main + ICON ${CMAKE_SOURCE_DIR}/src/frontend/backend/nx/icon.jpg + NACP main.nacp + ROMFS main_romfs + ) + + # compile and add shaders to romfs + set(SHADER_FOLDER ${CMAKE_SOURCE_DIR}/src/frontend/backend/nx/shaders) + + nx_add_shader_program(imgui_fsh ${SHADER_FOLDER}/imgui_fsh.glsl frag) + nx_add_shader_program(imgui_vsh ${SHADER_FOLDER}/imgui_vsh.glsl vert) + + dkp_install_assets(main_romfs + DESTINATION shaders + TARGETS + imgui_fsh + imgui_vsh + ) +endif() diff --git a/src/frontend/backend/CMakeLists.txt b/src/frontend/backend/CMakeLists.txt index e7f1b91..5f3d3d2 100644 --- a/src/frontend/backend/CMakeLists.txt +++ b/src/frontend/backend/CMakeLists.txt @@ -1,3 +1,7 @@ cmake_minimum_required(VERSION 3.20.0) -add_subdirectory(sdl2) +if (NINTENDO_SWITCH) + add_subdirectory(nx) +else() + add_subdirectory(sdl2) +endif() diff --git a/src/frontend/backend/sdl2/backend_sdl2.hpp b/src/frontend/backend/backend.hpp similarity index 88% rename from src/frontend/backend/sdl2/backend_sdl2.hpp rename to src/frontend/backend/backend.hpp index 48f8450..ae39fc4 100644 --- a/src/frontend/backend/sdl2/backend_sdl2.hpp +++ b/src/frontend/backend/backend.hpp @@ -2,12 +2,12 @@ // SPDX-License-Identifier: GPL-3.0-only #pragma once -#include "../../system.hpp" +#include "../system.hpp" #include #include #include -namespace sys::backend::sdl2 { +namespace sys::backend { [[nodiscard]] auto init() -> bool; auto quit() -> void; @@ -28,4 +28,4 @@ auto toggle_fullscreen() -> void; auto open_url(const char* url) -> void; -} // sys::backend::sdl2 +} // sys::backend diff --git a/src/frontend/backend/nx/CMakeLists.txt b/src/frontend/backend/nx/CMakeLists.txt new file mode 100644 index 0000000..012bb19 --- /dev/null +++ b/src/frontend/backend/nx/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.20.0) + +add_library(backend + backend_nx.cpp + ftpd_imgui/imgui_deko3d.cpp + ftpd_imgui/imgui_nx.cpp +) + +target_link_libraries(backend PRIVATE imgui) +target_link_libraries(backend PRIVATE GBA) +target_link_libraries(backend PRIVATE deko3dd) + +# why is this even needed??? +target_include_directories(backend PRIVATE ${DEVKITPRO}/portlibs/switch/include) + +target_apply_lto_in_release(backend) diff --git a/src/frontend/backend/nx/backend_nx.cpp b/src/frontend/backend/nx/backend_nx.cpp new file mode 100644 index 0000000..705cb65 --- /dev/null +++ b/src/frontend/backend/nx/backend_nx.cpp @@ -0,0 +1,591 @@ +// Copyright 2022 TotalJustice. +// SPDX-License-Identifier: GPL-3.0-only + +#include "ftpd_imgui/imgui_deko3d.h" +#include "ftpd_imgui/imgui_nx.h" +#include "../backend.hpp" +#include "../../system.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { + +#ifndef NDEBUG + #include // for close(socket); + static int nxlink_socket = 0; +#endif + +void userAppInit() +{ + appletLockExit(); // block exit until cleanup + + romfsInit(); // assets + plInitialize(PlServiceType_User); // fonts + +#ifndef NDEBUG + socketInitializeDefault(); + nxlink_socket = nxlinkStdio(); +#endif // NDEBUG +} + +void userAppExit() +{ +#ifndef NDEBUG + close(nxlink_socket); + socketExit(); +#endif // NDEBUG + + plExit(); + romfsExit(); + appletUnlockExit(); +} + +} // extern "C" + +namespace sys::backend { +namespace { + +struct Texture +{ + auto init() -> void; + auto quit() -> void; + auto update(std::uint16_t data[160][240]) -> void; + + [[nodiscard]] auto get_image_id() { return image_id; } + [[nodiscard]] auto get_sampler_id() { return sampler_id; } + +private: + dk::Image image; + dk::UniqueMemBlock memBlock; + + // todo: make texture creation like a factory which returns an + // id (index into texture array) + static inline const auto image_id = 2; + static inline const auto sampler_id = 1; + + // thse should be set in init() + static inline const auto format = DkImageFormat_RGB5_Unorm; + static inline const auto width = 240; + static inline const auto height = 160; + static inline const auto size = width * height * sizeof(uint16_t); +}; + +constexpr auto MAX_SAMPLERS = 2; +constexpr auto MAX_IMAGES = 8; +constexpr auto FB_NUM = 2u; +constexpr auto CMDBUF_SIZE = 1024 * 1024; + +unsigned s_width = 1920; +unsigned s_height = 1080; + +dk::UniqueDevice s_device; +dk::UniqueMemBlock s_depthMemBlock; +dk::Image s_depthBuffer; +dk::UniqueMemBlock s_fbMemBlock; +dk::Image s_frameBuffers[FB_NUM]; +dk::UniqueMemBlock s_cmdMemBlock[FB_NUM]; +dk::UniqueCmdBuf s_cmdBuf[FB_NUM]; +dk::UniqueMemBlock s_imageMemBlock; +dk::UniqueMemBlock s_descriptorMemBlock; +dk::SamplerDescriptor *s_samplerDescriptors = nullptr; +dk::ImageDescriptor *s_imageDescriptors = nullptr; + +dk::UniqueQueue s_queue; +dk::UniqueSwapchain s_swapchain; + +Texture textures[1]; +PadState pad; + +AppletHookCookie appletHookCookie; + +auto applet_show_error_message(const char* message, const char* long_message) +{ + ErrorApplicationConfig cfg; + errorApplicationCreate(&cfg, "Unsupported Launch!", "Please launch as application!"); + errorApplicationShow(&cfg); +} + +auto on_applet_focus_state() +{ + switch(appletGetFocusState()) + { + case AppletFocusState_InFocus: + std::printf("[APPLET] AppletFocusState_InFocus\n"); + break; + + case AppletFocusState_OutOfFocus: + std::printf("[APPLET] AppletFocusState_OutOfFocus\n"); + break; + + case AppletFocusState_Background: + std::printf("[APPLET] AppletFocusState_Background\n"); + break; + } +} + +auto on_applet_operation_mode() +{ + switch (appletGetOperationMode()) + { + case AppletOperationMode_Handheld: + std::printf("[APPLET] AppletOperationMode_Handheld\n"); + break; + + case AppletOperationMode_Console: + std::printf("[APPLET] AppletOperationMode_Console\n"); + break; + } +} + +auto applet_on_performance_mode() +{ + switch (appletGetPerformanceMode()) + { + case ApmPerformanceMode_Invalid: + std::printf("[APPLET] ApmPerformanceMode_Invalid\n"); + break; + + case ApmPerformanceMode_Normal: + std::printf("[APPLET] ApmPerformanceMode_Normal\n"); + break; + + case ApmPerformanceMode_Boost: + std::printf("[APPLET] ApmPerformanceMode_Boost\n"); + break; + } +} + +void appplet_hook_calback(AppletHookType type, void *param) +{ + switch (type) + { + case AppletHookType_OnFocusState: + on_applet_focus_state(); + break; + + case AppletHookType_OnOperationMode: + on_applet_operation_mode(); + break; + + case AppletHookType_OnPerformanceMode: + applet_on_performance_mode(); + break; + + case AppletHookType_OnExitRequest: + break; + + case AppletHookType_OnResume: + break; + + case AppletHookType_OnCaptureButtonShortPressed: + break; + + case AppletHookType_OnAlbumScreenShotTaken: + break; + + case AppletHookType_RequestToDisplay: + break; + + case AppletHookType_Max: + assert(!"AppletHookType_Max hit"); + break; + } +} + +void RebuildSwapchain(unsigned const width_, unsigned const height_) { + // destroy old swapchain + s_swapchain = nullptr; + + // create new depth buffer image layout + dk::ImageLayout depthLayout; + dk::ImageLayoutMaker{s_device} + .setFlags(DkImageFlags_UsageRender | DkImageFlags_HwCompression) + .setFormat(DkImageFormat_Z24S8) + .setDimensions(width_, height_) + .initialize(depthLayout); + + auto const depthAlign = depthLayout.getAlignment(); + auto const depthSize = depthLayout.getSize(); + + // create depth buffer memblock + if (!s_depthMemBlock) { + s_depthMemBlock = dk::MemBlockMaker{s_device, + imgui::deko3d::align(depthSize, std::max (depthAlign, DK_MEMBLOCK_ALIGNMENT))} + .setFlags(DkMemBlockFlags_GpuCached | DkMemBlockFlags_Image) + .create(); + } + + s_depthBuffer.initialize(depthLayout, s_depthMemBlock, 0); + + // create framebuffer image layout + dk::ImageLayout fbLayout; + dk::ImageLayoutMaker{s_device} + .setFlags(DkImageFlags_UsageRender | DkImageFlags_UsagePresent | DkImageFlags_HwCompression) + .setFormat(DkImageFormat_RGBA8_Unorm) + .setDimensions(width_, height_) + .initialize(fbLayout); + + auto const fbAlign = fbLayout.getAlignment(); + auto const fbSize = fbLayout.getSize(); + + // create framebuffer memblock + if (!s_fbMemBlock) { + s_fbMemBlock = dk::MemBlockMaker{s_device, imgui::deko3d::align(FB_NUM * fbSize, std::max (fbAlign, DK_MEMBLOCK_ALIGNMENT))} + .setFlags(DkMemBlockFlags_GpuCached | DkMemBlockFlags_Image) + .create(); + } + + // initialize swapchain images + std::array swapchainImages; + for (unsigned i = 0; i < FB_NUM; ++i) { + swapchainImages[i] = &s_frameBuffers[i]; + s_frameBuffers[i].initialize(fbLayout, s_fbMemBlock, i * fbSize); + } + + // create swapchain + s_swapchain = dk::SwapchainMaker{s_device, nwindowGetDefault(), swapchainImages}.create(); +} + +// void* userData, const char* context, DkResult result, const char* message +auto deko3d_error_cb(void* userData, const char* context, DkResult result, const char* message) +{ + switch (result) + { + case DkResult_Success: + return; + + case DkResult_Fail: + std::printf("[DkResult_Fail] %s\n", message); + return; + + case DkResult_Timeout: + std::printf("[DkResult_Timeout] %s\n", message); + return; + + case DkResult_OutOfMemory: + std::printf("[DkResult_OutOfMemory] %s\n", message); + return; + + case DkResult_NotImplemented: + std::printf("[DkResult_NotImplemented] %s\n", message); + return; + + case DkResult_MisalignedSize: + std::printf("[DkResult_MisalignedSize] %s\n", message); + return; + + case DkResult_MisalignedData: + std::printf("[DkResult_MisalignedData] %s\n", message); + return; + + case DkResult_BadInput: + std::printf("[DkResult_BadInput] %s\n", message); + return; + + case DkResult_BadFlags: + std::printf("[DkResult_BadFlags] %s\n", message); + return; + + case DkResult_BadState: + std::printf("[DkResult_BadState] %s\n", message); + return; + } +} + +void deko3d_init(void) { + // create deko3d device + s_device = dk::DeviceMaker{} + .setCbDebug(deko3d_error_cb) + .create(); + + // initialize swapchain with maximum resolution + RebuildSwapchain(s_width, s_height); + + // create memblocks for each image slot + for (std::size_t i = 0; i < FB_NUM; ++i) { + // create command buffer memblock + s_cmdMemBlock[i] = dk::MemBlockMaker{s_device, imgui::deko3d::align(CMDBUF_SIZE, DK_MEMBLOCK_ALIGNMENT)} + .setFlags(DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached) + .create(); + + // create command buffer + s_cmdBuf[i] = dk::CmdBufMaker{s_device}.create(); + s_cmdBuf[i].addMemory(s_cmdMemBlock[i], 0, s_cmdMemBlock[i].getSize()); + } + + // create image/sampler memblock + static_assert(sizeof(dk::ImageDescriptor) == DK_IMAGE_DESCRIPTOR_ALIGNMENT); + static_assert(sizeof(dk::SamplerDescriptor) == DK_SAMPLER_DESCRIPTOR_ALIGNMENT); + static_assert(DK_IMAGE_DESCRIPTOR_ALIGNMENT == DK_SAMPLER_DESCRIPTOR_ALIGNMENT); + s_descriptorMemBlock = dk::MemBlockMaker{s_device, imgui::deko3d::align((MAX_SAMPLERS + MAX_IMAGES) * sizeof(dk::ImageDescriptor), DK_MEMBLOCK_ALIGNMENT)} + .setFlags(DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached) + .create(); + + // get cpu address for descriptors + s_samplerDescriptors = static_cast (s_descriptorMemBlock.getCpuAddr()); + s_imageDescriptors = reinterpret_cast (&s_samplerDescriptors[MAX_SAMPLERS]); + + // create queue + s_queue = dk::QueueMaker{s_device}.setFlags(DkQueueFlags_Graphics).create(); + dk::UniqueCmdBuf &cmdBuf = s_cmdBuf[0]; + + // bind image/sampler descriptors + cmdBuf.bindSamplerDescriptorSet(s_descriptorMemBlock.getGpuAddr(), MAX_SAMPLERS); + cmdBuf.bindImageDescriptorSet(s_descriptorMemBlock.getGpuAddr() + MAX_SAMPLERS * sizeof(dk::SamplerDescriptor), MAX_IMAGES); + s_queue.submitCommands(cmdBuf.finishList()); + s_queue.waitIdle(); + cmdBuf.clear(); +} + +void exit_deko3d(void) { + // clean up all of the deko3d objects + s_imageMemBlock = nullptr; + s_descriptorMemBlock = nullptr; + + for (unsigned i = 0; i < FB_NUM; ++i) { + s_cmdBuf[i] = nullptr; + s_cmdMemBlock[i] = nullptr; + } + + s_queue = nullptr; + s_swapchain = nullptr; + s_fbMemBlock = nullptr; + s_depthMemBlock = nullptr; + s_device = nullptr; +} + +// SOURCE: https://github.com/joel16/NX-Shell/blob/5a5067afeb6b18c0d2bb4d7b16f71899a768012a/source/textures.cpp#L150 +auto Texture::init() -> void +{ + s_queue.waitIdle(); + + dk::ImageLayout layout; + dk::ImageLayoutMaker{s_device} + .setFlags(0) + .setFormat(format) + .setDimensions(width, height) + .initialize(layout); + + memBlock = dk::MemBlockMaker{s_device, imgui::deko3d::align(size, DK_MEMBLOCK_ALIGNMENT)} + .setFlags(DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached) + .create(); + + s_imageMemBlock = dk::MemBlockMaker{s_device, imgui::deko3d::align(layout.getSize(), DK_MEMBLOCK_ALIGNMENT)} + .setFlags(DkMemBlockFlags_GpuCached | DkMemBlockFlags_Image) + .create(); + + std::memset(memBlock.getCpuAddr(), 0, size); + + image.initialize(layout, s_imageMemBlock, 0); + s_imageDescriptors[image_id].initialize(image); + + dk::ImageView imageView(image); + + s_cmdBuf[0].copyBufferToImage({memBlock.getGpuAddr()}, imageView, + {0, 0, 0, static_cast(width), static_cast(height), 1}); + + s_queue.submitCommands(s_cmdBuf[0].finishList()); + + s_samplerDescriptors[sampler_id].initialize(dk::Sampler{} + .setFilter(DkFilter_Nearest, DkFilter_Nearest) + .setWrapMode(DkWrapMode_ClampToEdge, DkWrapMode_ClampToEdge, DkWrapMode_ClampToEdge)); + + s_queue.waitIdle(); +} + +auto Texture::update(std::uint16_t data[160][240]) -> void +{ + s_queue.waitIdle(); // is this needed? + + std::memcpy(memBlock.getCpuAddr(), data, size); + + dk::ImageView imageView(image); + + s_cmdBuf[0].copyBufferToImage({memBlock.getGpuAddr()}, imageView, + {0, 0, 0, static_cast(width), static_cast(height), 1}); + + s_queue.submitCommands(s_cmdBuf[0].finishList()); + + s_queue.waitIdle(); +} + +auto Texture::quit() -> void +{ + this->memBlock = nullptr; +} + +} // namespace + +[[nodiscard]] auto init() -> bool +{ + // only application applet supported + if (appletGetAppletType() != AppletType_Application) + { + applet_show_error_message("Unsupported Launch!", "Please launch as application!"); + return false; + } + + if (!imgui::nx::init()) + { + applet_show_error_message("Unsupported Launch!", "Please launch as application!"); + return false; + } + + // init deko3d + deko3d_init(); + + // init deko3d for imgui + imgui::deko3d::init(s_device, s_queue, s_cmdBuf[0], s_samplerDescriptors[0], s_imageDescriptors[0], dkMakeTextureHandle(0, 0), FB_NUM); + + // init all textures being used + for (auto& texture : textures) + { + texture.init(); + } + + // setup callback for applet events + appletHook(&appletHookCookie, appplet_hook_calback, nullptr); + + // setup controller + padConfigureInput(1, HidNpadStyleSet_NpadStandard); + padInitializeDefault(&pad); + + return true; +} + +auto quit() -> void +{ + imgui::nx::exit(); + + // wait for queue to be idle + s_queue.waitIdle(); + + for (auto& texture : textures) + { + texture.quit(); + } + + // close deko3d for imgui + imgui::deko3d::exit(); + + // close deko3d + exit_deko3d(); + + // unhook the applet callback + appletUnhook(&appletHookCookie); +} + + +auto poll_events() -> void +{ + if (!appletMainLoop()) + { + System::running = false; + return; + } + + padUpdate(&pad); + const auto down = padGetButtons(&pad); + + System::emu_set_button(gba::A, !!(down & HidNpadButton_A)); + System::emu_set_button(gba::B, !!(down & HidNpadButton_B)); + System::emu_set_button(gba::L, !!(down & HidNpadButton_L)); + System::emu_set_button(gba::R, !!(down & HidNpadButton_R)); + System::emu_set_button(gba::START, !!(down & HidNpadButton_Plus)); + System::emu_set_button(gba::SELECT, !!(down & HidNpadButton_Minus)); + System::emu_set_button(gba::UP, !!(down & HidNpadButton_AnyUp)); + System::emu_set_button(gba::DOWN, !!(down & HidNpadButton_AnyDown)); + System::emu_set_button(gba::LEFT, !!(down & HidNpadButton_AnyLeft)); + System::emu_set_button(gba::RIGHT, !!(down & HidNpadButton_AnyRight)); + + if (!!(down & HidNpadButton_ZR)) + { + System::running = false; + } + + // this only update inputs and screen size + // so it should be called in poll events + imgui::nx::newFrame(&pad); +} + +auto render_begin() -> void +{ + // imgui::nx::newFrame(&pad); +} + +auto render_end() -> void +{ + ImGuiIO &io = ImGui::GetIO(); + if (s_width != io.DisplaySize.x || s_height != io.DisplaySize.y) + { + s_width = io.DisplaySize.x; + s_height = io.DisplaySize.y; + RebuildSwapchain(s_width, s_height); + } + + // get image from queue + const int slot = s_queue.acquireImage(s_swapchain); + dk::UniqueCmdBuf &cmdBuf = s_cmdBuf[slot]; + cmdBuf.clear(); + + // bind frame/depth buffers and clear them + dk::ImageView colorTarget{s_frameBuffers[slot]}; + dk::ImageView depthTarget{s_depthBuffer}; + cmdBuf.bindRenderTargets(&colorTarget, &depthTarget); + cmdBuf.setScissors(0, DkScissor{0, 0, s_width, s_height}); + cmdBuf.clearColor(0, DkColorMask_RGBA, 0.0f, 0.0f, 0.0f, 1.0f); + cmdBuf.clearDepthStencil(true, 1.0f, 0xFF, 0); + s_queue.submitCommands(cmdBuf.finishList()); + + imgui::deko3d::render(s_device, s_queue, cmdBuf, slot); + + // wait for fragments to be completed before discarding depth/stencil buffer + cmdBuf.barrier(DkBarrier_Fragments, 0); + cmdBuf.discardDepthStencil(); + + // present image + s_queue.presentImage(s_swapchain, slot); +} + +auto get_texture(TextureID id) -> void* +{ + assert(id == TextureID::emu && "only emu texture is impl!"); + auto& texture = textures[std::to_underlying(id)]; + return imgui::deko3d::makeTextureID(dkMakeTextureHandle(texture.get_image_id(), texture.get_sampler_id())); +} + +auto update_texture(TextureID id, std::uint16_t pixels[160][240]) -> void +{ + assert(id == TextureID::emu && "only emu texture is impl!"); + textures[std::to_underlying(id)].update(pixels); +} + +[[nodiscard]] auto get_window_size() -> std::pair +{ + // todo: have callback for size changes + // return {s_width, s_height}; + return {1280, 720}; +} + +auto set_window_size(std::pair new_size) -> void +{ +} + +[[nodiscard]] auto is_fullscreen() -> bool +{ + return true; +} + +auto toggle_fullscreen() -> void +{ +} + +auto open_url(const char* url) -> void +{ +} + +} // sys::backend diff --git a/src/frontend/backend/nx/ftpd_imgui/imgui_deko3d.cpp b/src/frontend/backend/nx/ftpd_imgui/imgui_deko3d.cpp new file mode 100644 index 0000000..9f7d625 --- /dev/null +++ b/src/frontend/backend/nx/ftpd_imgui/imgui_deko3d.cpp @@ -0,0 +1,538 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// The MIT License (MIT) +// +// Copyright (C) 2020 Michael Theall +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define GLM_FORCE_DEFAULT_ALIGNED_GENTYPES +#define GLM_FORCE_INTRINSICS +#include +#include +#include + +#include "imgui_deko3d.h" + +namespace +{ +/// \brief Vertex buffer size +constexpr auto VTXBUF_SIZE = 1024u * 1024u; +/// \brief Index buffer size +constexpr auto IDXBUF_SIZE = 1024u * 1024u; + +/// \brief Vertex shader UBO +struct VertUBO +{ + /// \brief Projection matrix + glm::mat4 projMtx; +}; + +/// \brief Fragment shader UBO +struct FragUBO +{ + /// \brief Whether drawing a font or not + std::uint32_t font; +}; + +/// \brief Vertex attribute state +constexpr std::array VERTEX_ATTRIB_STATE = { + // clang-format off + DkVtxAttribState{0, 0, offsetof (ImDrawVert, pos), DkVtxAttribSize_2x32, DkVtxAttribType_Float, 0}, + DkVtxAttribState{0, 0, offsetof (ImDrawVert, uv), DkVtxAttribSize_2x32, DkVtxAttribType_Float, 0}, + DkVtxAttribState{0, 0, offsetof (ImDrawVert, col), DkVtxAttribSize_4x8, DkVtxAttribType_Unorm, 0}, + // clang-format on +}; + +/// \brief Vertex buffer state +constexpr std::array VERTEX_BUFFER_STATE = { + DkVtxBufferState{sizeof (ImDrawVert), 0}, +}; + +/// \brief Shader code memblock +dk::UniqueMemBlock s_codeMemBlock; +/// \brief Shaders (vertex, fragment) +dk::Shader s_shaders[2]; + +/// \brief UBO memblock +dk::UniqueMemBlock s_uboMemBlock; + +/// \brief Vertex data memblock +std::vector s_vtxMemBlock; +/// \brief Index data memblock +std::vector s_idxMemBlock; + +/// \brief Font image memblock +dk::UniqueMemBlock s_fontImageMemBlock; +/// \brief Font texture handle +DkResHandle s_fontTextureHandle; + +/// \brief Load shader code +void loadShaders (dk::UniqueDevice &device_) +{ + /// \brief Shader file descriptor + struct ShaderFile + { + /// \brief Parameterized constructor + /// \param shader_ Shader object + /// \param path_ Path to source code + ShaderFile (dk::Shader &shader_, char const *const path_) + : shader (shader_), path (path_), size (getSize (path_)) + { + } + + /// \brief Get size of a file + /// \param path_ Path to file + static std::size_t getSize (char const *const path_) + { + struct stat st; + auto const rc = ::stat (path_, &st); + if (rc != 0) + { + std::fprintf (stderr, "stat(%s): %s\n", path_, std::strerror (errno)); + std::abort (); + } + + return st.st_size; + } + + /// \brief Shader object + dk::Shader &shader; + /// \brief Path to source code + char const *const path; + /// \brief Source code file size + std::size_t const size; + }; + + auto shaderFiles = {ShaderFile{s_shaders[0], "romfs:/shaders/imgui_vsh.dksh"}, + ShaderFile{s_shaders[1], "romfs:/shaders/imgui_fsh.dksh"}}; + + // calculate total size of shaders + auto const codeSize = std::accumulate (std::begin (shaderFiles), + std::end (shaderFiles), + DK_SHADER_CODE_UNUSABLE_SIZE, + [] (auto const sum_, auto const &file_) { + return sum_ + imgui::deko3d::align (file_.size, DK_SHADER_CODE_ALIGNMENT); + }); + + // create shader code memblock + s_codeMemBlock = + dk::MemBlockMaker{device_, imgui::deko3d::align (codeSize, DK_MEMBLOCK_ALIGNMENT)} + .setFlags ( + DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached | DkMemBlockFlags_Code) + .create (); + + auto const addr = static_cast (s_codeMemBlock.getCpuAddr ()); + std::size_t offset = 0; + + // read shaders into memblock + for (auto &file : shaderFiles) + { + std::uint32_t const codeOffset = offset; + + auto *fp = fopen(file.path, "rb"); + if (!fp) + { + std::fprintf (stderr, "open(%s): %s\n", file.path, std::strerror (errno)); + std::abort (); + } + + if (fread (&addr[offset], 1, file.size, fp) != file.size) + { + std::fprintf (stderr, "read(%s): %s\n", file.path, std::strerror (errno)); + std::abort (); + } + + fclose(fp); + + dk::ShaderMaker{s_codeMemBlock, codeOffset}.initialize (file.shader); + + offset = imgui::deko3d::align (offset + file.size, DK_SHADER_CODE_ALIGNMENT); + } +} + +/// \brief Setup render state +/// \param cmdBuf_ Command buffer +/// \param drawData_ Data to draw +/// \param width_ Framebuffer width +/// \param height_ Framebuffer height +DkCmdList setupRenderState (dk::UniqueCmdBuf &cmdBuf_, + ImDrawData *const drawData_, + unsigned const width_, + unsigned const height_) +{ + // setup viewport, orthographic projection matrix + // our visible imgui space lies from drawData_->DisplayPos (top left) to + // drawData_->DisplayPos+data_data->DisplaySize (bottom right). DisplayPos is (0,0) for single + // viewport apps. + auto const L = drawData_->DisplayPos.x; + auto const R = drawData_->DisplayPos.x + drawData_->DisplaySize.x; + auto const T = drawData_->DisplayPos.y; + auto const B = drawData_->DisplayPos.y + drawData_->DisplaySize.y; + + VertUBO vertUBO; + vertUBO.projMtx = glm::orthoRH_ZO (L, R, B, T, -1.0f, 1.0f); + + // create command buffer to initialize/reset render state + cmdBuf_.setViewports (0, + DkViewport{0.0f, 0.0f, static_cast(width_), static_cast(height_)}); + cmdBuf_.bindShaders (DkStageFlag_GraphicsMask, {&s_shaders[0], &s_shaders[1]}); + cmdBuf_.bindUniformBuffer (DkStage_Vertex, + 0, + s_uboMemBlock.getGpuAddr (), + imgui::deko3d::align (sizeof (VertUBO), DK_UNIFORM_BUF_ALIGNMENT)); + cmdBuf_.pushConstants (s_uboMemBlock.getGpuAddr (), + imgui::deko3d::align (sizeof (VertUBO), DK_UNIFORM_BUF_ALIGNMENT), + 0, + sizeof (VertUBO), + &vertUBO); + cmdBuf_.bindUniformBuffer (DkStage_Fragment, + 0, + s_uboMemBlock.getGpuAddr () + + imgui::deko3d::align (sizeof (VertUBO), DK_UNIFORM_BUF_ALIGNMENT), + imgui::deko3d::align (sizeof (FragUBO), DK_UNIFORM_BUF_ALIGNMENT)); + cmdBuf_.bindRasterizerState (dk::RasterizerState{}.setCullMode (DkFace_None)); + cmdBuf_.bindColorState (dk::ColorState{}.setBlendEnable (0, true)); + cmdBuf_.bindColorWriteState (dk::ColorWriteState{}); + cmdBuf_.bindDepthStencilState (dk::DepthStencilState{}.setDepthTestEnable (false)); + cmdBuf_.bindBlendStates (0, + dk::BlendState{}.setFactors (DkBlendFactor_SrcAlpha, + DkBlendFactor_InvSrcAlpha, + DkBlendFactor_InvSrcAlpha, + DkBlendFactor_Zero)); + cmdBuf_.bindVtxAttribState (VERTEX_ATTRIB_STATE); + cmdBuf_.bindVtxBufferState (VERTEX_BUFFER_STATE); + + return cmdBuf_.finishList (); +} +} + +void imgui::deko3d::init (dk::UniqueDevice &device_, + dk::UniqueQueue &queue_, + dk::UniqueCmdBuf &cmdBuf_, + dk::SamplerDescriptor &samplerDescriptor_, + dk::ImageDescriptor &imageDescriptor_, + DkResHandle fontTextureHandle_, + unsigned const imageCount_) +{ + auto &io = ImGui::GetIO (); + + // setup back-end capabilities flags + io.BackendRendererName = "deko3d"; + io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; + + // load shader code + loadShaders (device_); + + // create UBO memblock + s_uboMemBlock = dk::MemBlockMaker{device_, + align (align (sizeof (VertUBO), DK_UNIFORM_BUF_ALIGNMENT) + + align (sizeof (FragUBO), DK_UNIFORM_BUF_ALIGNMENT), + DK_MEMBLOCK_ALIGNMENT)} + .setFlags (DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached) + .create (); + + // create memblocks for each image slot + for (std::size_t i = 0; i < imageCount_; ++i) + { + // create vertex data memblock + s_vtxMemBlock.emplace_back ( + dk::MemBlockMaker{device_, align (VTXBUF_SIZE, DK_MEMBLOCK_ALIGNMENT)} + .setFlags (DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached) + .create ()); + + // create index data memblock + s_idxMemBlock.emplace_back ( + dk::MemBlockMaker{device_, align (IDXBUF_SIZE, DK_MEMBLOCK_ALIGNMENT)} + .setFlags (DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached) + .create ()); + } + + // get texture atlas + io.Fonts->SetTexID (makeTextureID (fontTextureHandle_)); + s_fontTextureHandle = fontTextureHandle_; + unsigned char *pixels; + int width; + int height; + io.Fonts->GetTexDataAsAlpha8 (&pixels, &width, &height); + + // create memblock for transfer + dk::UniqueMemBlock memBlock = + dk::MemBlockMaker{device_, align (width * height, DK_MEMBLOCK_ALIGNMENT)} + .setFlags (DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached) + .create (); + std::memcpy (memBlock.getCpuAddr (), pixels, width * height); + + // initialize sampler descriptor + samplerDescriptor_.initialize ( + dk::Sampler{} + .setFilter (DkFilter_Linear, DkFilter_Linear) + .setWrapMode (DkWrapMode_ClampToEdge, DkWrapMode_ClampToEdge, DkWrapMode_ClampToEdge)); + + // initialize texture atlas image layout + dk::ImageLayout layout; + dk::ImageLayoutMaker{device_} + .setFlags (0) + .setFormat (DkImageFormat_R8_Unorm) + .setDimensions (width, height) + .initialize (layout); + + auto const fontAlign = layout.getAlignment (); + auto const fontSize = layout.getSize (); + + // create image memblock + s_fontImageMemBlock = dk::MemBlockMaker{device_, + align (fontSize, std::max (fontAlign, DK_MEMBLOCK_ALIGNMENT))} + .setFlags (DkMemBlockFlags_GpuCached | DkMemBlockFlags_Image) + .create (); + + // initialize font texture atlas image descriptor + dk::Image fontTexture; + fontTexture.initialize (layout, s_fontImageMemBlock, 0); + imageDescriptor_.initialize (fontTexture); + + // copy font texture atlas to image view + dk::ImageView imageView{fontTexture}; + cmdBuf_.copyBufferToImage ({memBlock.getGpuAddr ()}, imageView, + {0, 0, 0, static_cast(width), static_cast(height), 1}); + + // submit commands to transfer font texture + queue_.submitCommands (cmdBuf_.finishList ()); + + // wait for commands to complete before releasing memblock + queue_.waitIdle (); +} + +void imgui::deko3d::exit () +{ + s_fontImageMemBlock = nullptr; + + s_idxMemBlock.clear (); + s_vtxMemBlock.clear (); + + s_uboMemBlock = nullptr; + s_codeMemBlock = nullptr; +} + +void imgui::deko3d::render (dk::UniqueDevice &device_, + dk::UniqueQueue &queue_, + dk::UniqueCmdBuf &cmdBuf_, + unsigned const slot_) +{ + // get ImGui draw data + auto const drawData = ImGui::GetDrawData (); + if (drawData->CmdListsCount <= 0) + return; + + // get framebuffer dimensions + unsigned width = drawData->DisplaySize.x * drawData->FramebufferScale.x; + unsigned height = drawData->DisplaySize.y * drawData->FramebufferScale.y; + if (width <= 0 || height <= 0) + return; + + // setup desired render state + auto const setupCmd = setupRenderState (cmdBuf_, drawData, width, height); + queue_.submitCommands (setupCmd); + + // currently bound texture + std::optional boundTextureHandle; + + // will project scissor/clipping rectangles into framebuffer space + // (0,0) unless using multi-viewports + auto const clipOff = drawData->DisplayPos; + // (1,1) unless using retina display which are often (2,2) + auto const clipScale = drawData->FramebufferScale; + + // check if we need to grow vertex data memblock + if (s_vtxMemBlock[slot_].getSize () < drawData->TotalVtxCount * sizeof (ImDrawVert)) + { + // add 10% to avoid growing many frames in a row + std::size_t const count = drawData->TotalVtxCount * 1.1f; + + s_vtxMemBlock[slot_] = + dk::MemBlockMaker{device_, align (count * sizeof (ImDrawVert), DK_MEMBLOCK_ALIGNMENT)} + .setFlags (DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached) + .create (); + } + + // check if we need to grow index data memblock + if (s_idxMemBlock[slot_].getSize () < drawData->TotalIdxCount * sizeof (ImDrawIdx)) + { + // add 10% to avoid growing many frames in a row + std::size_t const count = drawData->TotalIdxCount * 1.1f; + + s_idxMemBlock[slot_] = + dk::MemBlockMaker{device_, align (count * sizeof (ImDrawIdx), DK_MEMBLOCK_ALIGNMENT)} + .setFlags (DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached) + .create (); + } + + // get base cpu addresses + auto const cpuVtx = static_cast (s_vtxMemBlock[slot_].getCpuAddr ()); + auto const cpuIdx = static_cast (s_idxMemBlock[slot_].getCpuAddr ()); + + // get base gpu addresses + auto const gpuVtx = s_vtxMemBlock[slot_].getGpuAddr (); + auto const gpuIdx = s_idxMemBlock[slot_].getGpuAddr (); + + // get memblock sizes + auto const sizeVtx = s_vtxMemBlock[slot_].getSize (); + auto const sizeIdx = s_idxMemBlock[slot_].getSize (); + + // bind vertex/index data memblocks + static_assert (sizeof (ImDrawIdx) == sizeof (std::uint16_t)); + cmdBuf_.bindVtxBuffer (0, gpuVtx, sizeVtx); + cmdBuf_.bindIdxBuffer (DkIdxFormat_Uint16, gpuIdx); + + // render command lists + std::size_t offsetVtx = 0; + std::size_t offsetIdx = 0; + for (int i = 0; i < drawData->CmdListsCount; ++i) + { + auto const &cmdList = *drawData->CmdLists[i]; + + auto const vtxSize = cmdList.VtxBuffer.Size * sizeof (ImDrawVert); + auto const idxSize = cmdList.IdxBuffer.Size * sizeof (ImDrawIdx); + + // double check that we don't overrun vertex data memblock + if (sizeVtx - offsetVtx < vtxSize) + { + std::fprintf (stderr, "Not enough vertex buffer\n"); + std::fprintf (stderr, "\t%zu/%u used, need %zu\n", offsetVtx, sizeVtx, vtxSize); + continue; + } + + // double check that we don't overrun index data memblock + if (sizeIdx - offsetIdx < idxSize) + { + std::fprintf (stderr, "Not enough index buffer\n"); + std::fprintf (stderr, "\t%zu/%u used, need %zu\n", offsetIdx, sizeIdx, idxSize); + continue; + } + + // copy vertex/index data into memblocks + std::memcpy (cpuVtx + offsetVtx, cmdList.VtxBuffer.Data, vtxSize); + std::memcpy (cpuIdx + offsetIdx, cmdList.IdxBuffer.Data, idxSize); + + for (auto const &cmd : cmdList.CmdBuffer) + { + if (cmd.UserCallback) + { + // submit commands to preserve ordering + queue_.submitCommands (cmdBuf_.finishList ()); + + // user callback, registered via ImDrawList::AddCallback() + // (ImDrawCallback_ResetRenderState is a special callback value used by the user to + // request the renderer to reset render state.) + if (cmd.UserCallback == ImDrawCallback_ResetRenderState) + queue_.submitCommands (setupCmd); + else + cmd.UserCallback (&cmdList, &cmd); + } + else + { + // project scissor/clipping rectangles into framebuffer space + ImVec4 clip; + clip.x = (cmd.ClipRect.x - clipOff.x) * clipScale.x; + clip.y = (cmd.ClipRect.y - clipOff.y) * clipScale.y; + clip.z = (cmd.ClipRect.z - clipOff.x) * clipScale.x; + clip.w = (cmd.ClipRect.w - clipOff.y) * clipScale.y; + + // check if clip coordinate are outside of the framebuffer + if (clip.x >= width || clip.y >= height || clip.z < 0.0f || clip.w < 0.0f) + continue; + + // keep scissor coordinates inside viewport + if (clip.x < 0.0f) + clip.x = 0.0f; + if (clip.y < 0.0f) + clip.y = 0.0f; + if (clip.z > width) + clip.z = width; + if (clip.w > height) + clip.z = height; + + // apply scissor boundaries + cmdBuf_.setScissors (0, + DkScissor{static_cast(clip.x), static_cast(clip.y), + static_cast(clip.z - clip.x), static_cast(clip.w - clip.y)}); + + // get texture handle + auto const textureHandle = reinterpret_cast (cmd.TextureId); + + // check if we need to bind a new texture + if (!boundTextureHandle || textureHandle != *boundTextureHandle) + { + // check if this is the first draw or changing to or from the font texture + if (!boundTextureHandle || textureHandle == s_fontTextureHandle || + *boundTextureHandle == s_fontTextureHandle) + { + FragUBO fragUBO; + fragUBO.font = (textureHandle == s_fontTextureHandle); + + // update fragment shader UBO + cmdBuf_.pushConstants ( + s_uboMemBlock.getGpuAddr () + + align (sizeof (VertUBO), DK_UNIFORM_BUF_ALIGNMENT), + align (sizeof (FragUBO), DK_UNIFORM_BUF_ALIGNMENT), + 0, + sizeof (FragUBO), + &fragUBO); + } + + boundTextureHandle = textureHandle; + + // bind the new texture + cmdBuf_.bindTextures (DkStage_Fragment, 0, textureHandle); + } + + // draw the draw list + cmdBuf_.drawIndexed (DkPrimitive_Triangles, + cmd.ElemCount, + 1, + cmd.IdxOffset + offsetIdx / sizeof (ImDrawIdx), + cmd.VtxOffset + offsetVtx / sizeof (ImDrawVert), + 0); + } + } + + offsetVtx += vtxSize; + offsetIdx += idxSize; + } + + // submit final commands + queue_.submitCommands (cmdBuf_.finishList ()); +} diff --git a/src/frontend/backend/nx/ftpd_imgui/imgui_deko3d.h b/src/frontend/backend/nx/ftpd_imgui/imgui_deko3d.h new file mode 100644 index 0000000..63c9e75 --- /dev/null +++ b/src/frontend/backend/nx/ftpd_imgui/imgui_deko3d.h @@ -0,0 +1,87 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// The MIT License (MIT) +// +// Copyright (C) 2020 Michael Theall +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#pragma once + +#ifndef CLASSIC +#include + +#include + +namespace imgui +{ +namespace deko3d +{ +/// \brief Initialize deko3d +/// \param device_ deko3d device (used to allocate vertex/index and font texture buffers) +/// \param queue_ deko3d queue (used to run command lists) +/// \param cmdBuf_ Command buffer (used to build command lists) +/// \param[out] samplerDescriptor_ Sampler descriptor for font texture +/// \param[out] imageDescriptor_ Image descriptor for font texture +/// \param fontTextureHandle_ Texture handle that references samplerDescriptor_ and imageDescriptor_ +/// \param imageCount_ Images in the swapchain +void init (dk::UniqueDevice &device_, + dk::UniqueQueue &queue_, + dk::UniqueCmdBuf &cmdBuf_, + dk::SamplerDescriptor &samplerDescriptor_, + dk::ImageDescriptor &imageDescriptor_, + DkResHandle fontTextureHandle_, + unsigned imageCount_); + +/// \brief Deinitialize deko3d +void exit (); + +/// \brief Render ImGui draw list +/// \param device_ deko3d device (used to reallocate vertex/index buffers) +/// \param queue_ deko3d queue (used to run command lists) +/// \param cmdBuf_ Command buffer (used to build command lists) +/// \param slot_ Image slot +void render (dk::UniqueDevice &device_, + dk::UniqueQueue &queue_, + dk::UniqueCmdBuf &cmdBuf_, + unsigned slot_); + +/// \brief Make ImGui texture id from deko3d texture handle +/// \param handle_ Texture handle +inline void *makeTextureID (DkResHandle handle_) +{ + return reinterpret_cast (static_cast (handle_)); +} + +/// \brief Align power-of-two value +/// \tparam T Value type +/// \tparam U Alignment type +/// \param size_ Value to align +/// \param align_ Alignment +template +constexpr inline std::uint32_t align (T const &size_, U const &align_) +{ + return static_cast (size_ + align_ - 1) & ~(align_ - 1); +} +} +} +#endif diff --git a/src/frontend/backend/nx/ftpd_imgui/imgui_nx.cpp b/src/frontend/backend/nx/ftpd_imgui/imgui_nx.cpp new file mode 100644 index 0000000..1cc0782 --- /dev/null +++ b/src/frontend/backend/nx/ftpd_imgui/imgui_nx.cpp @@ -0,0 +1,209 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// The MIT License (MIT) +// +// Copyright (C) 2020 Michael Theall +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include "imgui_nx.h" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + +namespace { + +std::chrono::steady_clock::time_point s_lastMouseUpdate; + +float s_width = 1280.0f; +float s_height = 720.0f; + +ImVec2 s_mousePos = ImVec2(0.0f, 0.0f); + +AppletHookCookie s_appletHookCookie; + +auto on_applet_operation_mode() +{ + switch (appletGetOperationMode()) + { + case AppletOperationMode_Handheld: + // use handheld mode resolution (720p) and scale + s_width = 1280.0f, s_height = 720.0f; + ImGui::GetStyle().ScaleAllSizes(1.9f / 2.6f); + ImGui::GetIO().FontGlobalScale = 0.9f; + break; + + case AppletOperationMode_Console: + // use docked mode resolution (1080p) and scale + s_width = 1920.0f, s_height = 1080.0f; + ImGui::GetStyle().ScaleAllSizes(2.6f / 1.9f); + ImGui::GetIO().FontGlobalScale = 1.6f; + break; + } +} + +void handleAppletHook(AppletHookType type, void *param) +{ + if (type != AppletHookType_OnOperationMode) + { + return; + } +} + +void updateTouch(ImGuiIO &io_) { + // read touch positions + HidTouchScreenState state = {0}; + auto count = hidGetTouchScreenStates(&state, 1); + if (count < 1 || state.count < 1) { + io_.MouseDown[0] = false; + return; + } + + // set mouse position to touch point + s_mousePos = ImVec2(state.touches[0].x, state.touches[0].y); + io_.MouseDown[0] = true; +} + +void updateKeys(PadState* pad, ImGuiIO &io_) { + constexpr std::array mapping = { + std::pair(ImGuiNavInput_Activate, HidNpadButton_A), + std::pair(ImGuiNavInput_Cancel, HidNpadButton_B), + std::pair(ImGuiNavInput_Input, HidNpadButton_X), + //std::pair(ImGuiNavInput_Menu, HidNpadButton_Y), + std::pair(ImGuiNavInput_FocusPrev, HidNpadButton_L), + std::pair(ImGuiNavInput_TweakSlow, HidNpadButton_L), + std::pair(ImGuiNavInput_FocusNext, HidNpadButton_R), + std::pair(ImGuiNavInput_TweakFast, HidNpadButton_R), + std::pair(ImGuiNavInput_DpadUp, HidNpadButton_Up), + std::pair(ImGuiNavInput_DpadRight, HidNpadButton_Right), + std::pair(ImGuiNavInput_DpadDown, HidNpadButton_Down), + std::pair(ImGuiNavInput_DpadLeft, HidNpadButton_Left), + }; + + const auto down = padGetButtons(pad); + + for (auto [im, nx]: mapping) + if (down & nx) + io_.NavInputs[im] = 1.0f; +} + +} // namespace + +bool imgui::nx::init() { + // update scaling on init! + on_applet_operation_mode(); + + auto &io = ImGui::GetIO(); + + // Load nintendo font + PlFontData standard, extended; + static ImWchar extended_range[] = {0xe000, 0xe152}; + if (R_SUCCEEDED(plGetSharedFontByType(&standard, PlSharedFontType_Standard)) && + R_SUCCEEDED(plGetSharedFontByType(&extended, PlSharedFontType_NintendoExt))) + { + std::uint8_t *px; + int w, h, bpp; + ImFontConfig font_cfg; + + font_cfg.FontDataOwnedByAtlas = false; + io.Fonts->AddFontFromMemoryTTF(standard.address, standard.size, 24.0f, &font_cfg, io.Fonts->GetGlyphRangesDefault()); + font_cfg.MergeMode = true; + io.Fonts->AddFontFromMemoryTTF(extended.address, extended.size, 24.0f, &font_cfg, extended_range); + + // build font atlas + io.Fonts->GetTexDataAsAlpha8(&px, &w, &h, &bpp); + io.Fonts->Flags |= ImFontAtlasFlags_NoPowerOfTwoHeight; + io.Fonts->Build(); + } + + auto &style = ImGui::GetStyle(); + style.WindowRounding = 0.0f; + + const auto mode = appletGetOperationMode(); + if (mode == AppletOperationMode_Handheld) { + s_width = 1280.0f, s_height = 720.0f; + style.ScaleAllSizes(1.9f); + io.FontGlobalScale = 0.9f; + } else { + s_width = 1920.0f, s_height = 1080.0f; + style.ScaleAllSizes(2.6f); + io.FontGlobalScale = 1.6f; + } + + // initialize applet hooks + appletHook(&s_appletHookCookie, handleAppletHook, nullptr); + + // disable imgui.ini file + io.IniFilename = nullptr; + + // setup config flags + io.ConfigFlags |= ImGuiConfigFlags_IsTouchScreen; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; + + io.BackendFlags |= ImGuiBackendFlags_HasGamepad; + + // disable mouse cursor + io.MouseDrawCursor = false; + + return true; +} + +void imgui::nx::newFrame(PadState* pad) { + auto &io = ImGui::GetIO(); + + // setup display metrics + io.DisplaySize = ImVec2(s_width, s_height); + io.DisplayFramebufferScale = ImVec2(1.0f, 1.0f); + + // time step + static auto const start = std::chrono::steady_clock::now(); + static auto prev = start; + auto const now = std::chrono::steady_clock::now(); + + io.DeltaTime = std::chrono::duration (now - prev).count(); + prev = now; + + // update inputs + updateTouch(io); + updateKeys(pad, io); + + // clamp mouse to screen + s_mousePos.x = std::clamp(s_mousePos.x, 0.0f, s_width); + s_mousePos.y = std::clamp(s_mousePos.y, 0.0f, s_height); + io.MousePos = s_mousePos; +} + +void imgui::nx::exit() { + // deinitialize applet hooks + appletUnhook(&s_appletHookCookie); +} diff --git a/src/frontend/backend/nx/ftpd_imgui/imgui_nx.h b/src/frontend/backend/nx/ftpd_imgui/imgui_nx.h new file mode 100644 index 0000000..90d7dc6 --- /dev/null +++ b/src/frontend/backend/nx/ftpd_imgui/imgui_nx.h @@ -0,0 +1,39 @@ +// ftpd is a server implementation based on the following: +// - RFC 959 (https://tools.ietf.org/html/rfc959) +// - RFC 3659 (https://tools.ietf.org/html/rfc3659) +// - suggested implementation details from https://cr.yp.to/ftp/filesystem.html +// +// The MIT License (MIT) +// +// Copyright (C) 2020 Michael Theall +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#pragma once + +#include +#include + +namespace imgui::nx { + +bool init(); +void exit(); +void newFrame(PadState* pad); + +} // namespace imgui::nx diff --git a/src/frontend/backend/nx/icon.jpg b/src/frontend/backend/nx/icon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7df95de2795a996a312a8519dbeace68d6b656f9 GIT binary patch literal 1229 zcmex=>ukC3pCfH06P05XITq?4J21E^7eo0A(TN+S4wfI*N0 zr1V9`}q+eCsuEF`@Q5J6_$2h2bzz9fh*m{x~L1bQ&=Y;WN8KsJE^VmnZrf#FI6 z+#HBqM)w#PfQBGD17vvmACQ$<4l+kPZ?6HeSi{yBEVh3DWC0Z;xddd0R~isL0lUMk{s2&zZ3*KUm$|z@%+RdET*m)^3 @@ -11,7 +11,7 @@ #include #include -namespace sys::backend::sdl2 { +namespace sys::backend { namespace { SDL_Window* window{}; @@ -574,4 +574,4 @@ auto open_url(const char* url) -> void SDL_OpenURL(url); } -} // sys::backend::sdl2 +} // sys::backend diff --git a/src/frontend/main.cpp b/src/frontend/main.cpp index 4015cd2..42d7789 100644 --- a/src/frontend/main.cpp +++ b/src/frontend/main.cpp @@ -7,6 +7,7 @@ auto main(int argc, char** argv) -> int { + #if !defined(__SWITCH__) if (argc < 2) { std::printf("- args: exe rom\n"); @@ -14,6 +15,7 @@ auto main(int argc, char** argv) -> int std::printf("- args: exe rom bios\n"); return 1; } + #endif auto system = std::make_unique(); diff --git a/src/frontend/system.cpp b/src/frontend/system.cpp index 9b7604d..5ee5b4f 100644 --- a/src/frontend/system.cpp +++ b/src/frontend/system.cpp @@ -22,9 +22,7 @@ #include #include -#include "backend/sdl2/backend_sdl2.hpp" - -namespace bend = sys::backend::sdl2; +#include "backend/backend.hpp" namespace sys { @@ -183,7 +181,7 @@ auto System::render_layers() -> void continue; } - bend::update_texture(layers[layer].id, layers[layer].pixels); + backend::update_texture(layers[layer].id, layers[layer].pixels); const auto flags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoNav; ImGui::SetNextWindowSize(ImVec2(240, 160)); @@ -201,7 +199,11 @@ auto System::render_layers() -> void ImGui::SetCursorPos({0, 0}); ImVec2 p = ImGui::GetCursorScreenPos(); - ImGui::Image(bend::get_texture(layers[layer].id), ImVec2(240, 160)); + auto texure = backend::get_texture(layers[layer].id); + if (texure) + { + ImGui::Image(backend::get_texture(layers[layer].id), ImVec2(240, 160)); + } ImGui::PopStyleVar(5); if (show_grid) @@ -219,7 +221,7 @@ System::~System() // save game on exit System::closerom(); - bend::quit(); + backend::quit(); // Cleanup ImGui::DestroyContext(); @@ -323,6 +325,13 @@ auto System::emu_set_button(gba::Button button, bool down) -> void auto System::init(int argc, char** argv) -> bool { +#if defined(__SWITCH__) + if (!System::loadrom("/roms/gba/doom.gba")) + { + std::printf("failed to loadrom\n"); + return false; + } +#else if (argc < 2) { return false; @@ -347,6 +356,7 @@ auto System::init(int argc, char** argv) -> bool return false; } } +#endif // set audio callback and user data // System::gameboy_advance.set_userdata(this); @@ -364,14 +374,16 @@ auto System::init(int argc, char** argv) -> bool ImGui::StyleColorsDark(); //ImGui::StyleColorsClassic(); + #if !defined(__SWITCH__) io.Fonts->AddFontFromMemoryCompressedTTF(trim_font_compressed_data, trim_font_compressed_size, 20); + #endif - return bend::init(); + return backend::init(); } auto System::run_events() -> void { - bend::poll_events(); + backend::poll_events(); } auto System::run_emu() -> void @@ -524,11 +536,11 @@ auto System::menubar_tab_help() -> void if (ImGui::MenuItem("Info")) {} if (ImGui::MenuItem("Open On GitHub")) { - bend::open_url("https://github.com/ITotalJustice/notorious_beeg"); + backend::open_url("https://github.com/ITotalJustice/notorious_beeg"); } if (ImGui::MenuItem("Open An Issue")) { - bend::open_url("https://github.com/ITotalJustice/notorious_beeg/issues/new"); + backend::open_url("https://github.com/ITotalJustice/notorious_beeg/issues/new"); } } @@ -642,7 +654,7 @@ auto System::emu_update_texture() -> void return; } - bend::update_texture(TextureID::emu, gameboy_advance.ppu.pixels); + backend::update_texture(TextureID::emu, gameboy_advance.ppu.pixels); } auto System::emu_render() -> void @@ -664,7 +676,11 @@ auto System::emu_render() -> void ImGui::SetCursorPos(ImVec2(0, 0)); ImVec2 p = ImGui::GetCursorScreenPos(); - ImGui::Image(bend::get_texture(TextureID::emu), ImVec2(emu_rect.w, emu_rect.h)); + auto texture = backend::get_texture(TextureID::emu); + if (texture != nullptr) + { + ImGui::Image(texture, ImVec2(emu_rect.w, emu_rect.h)); + } ImGui::PopStyleVar(5); if (show_grid) @@ -679,7 +695,7 @@ auto System::emu_render() -> void auto System::run_render() -> void { // Start the Dear ImGui frame - bend::render_begin(); + backend::render_begin(); ImGui::NewFrame(); // 1. Show the big demo window (Most of the sample code is in ImGui::ShowDemoWindow()! You can browse its code to learn more about Dear ImGui!). @@ -707,7 +723,7 @@ auto System::run_render() -> void // Rendering (REMEMBER TO RENDER IMGUI STUFF [BEFORE] THIS LINE) ImGui::Render(); - bend::render_end(); + backend::render_end(); } auto System::run() -> void @@ -735,12 +751,12 @@ auto System::run() -> void auto System::is_fullscreen() -> bool { - return bend::is_fullscreen(); + return backend::is_fullscreen(); } auto System::toggle_fullscreen() -> void { - bend::toggle_fullscreen(); + backend::toggle_fullscreen(); } auto System::resize_to_menubar() -> void @@ -752,15 +768,15 @@ auto System::resize_to_menubar() -> void should_resize = false; - const auto [w, h] = bend::get_window_size(); - bend::set_window_size({w, h + menubar_height}); + const auto [w, h] = backend::get_window_size(); + backend::set_window_size({w, h + menubar_height}); System::resize_emu_screen(); } auto System::resize_emu_screen() -> void { - const auto [w, h] = bend::get_window_size(); + const auto [w, h] = backend::get_window_size(); // update rect emu_rect.x = 0; From dbe5384724ce97e82c9a0a0185c0818d9ca6d986 Mon Sep 17 00:00:00 2001 From: ITotalJustice <47043333+ITotalJustice@users.noreply.github.com> Date: Mon, 25 Apr 2022 19:54:09 +0100 Subject: [PATCH 2/3] [nx] add audio seems to sound very good! not perfect yet because not handling too many samples generated and need a better way to handle underflow. see #70 --- src/frontend/backend/nx/CMakeLists.txt | 2 + src/frontend/backend/nx/audio/audio.cpp | 267 ++++++++++++++++++++++++ src/frontend/backend/nx/audio/audio.hpp | 10 + src/frontend/backend/nx/backend_nx.cpp | 43 ++-- 4 files changed, 308 insertions(+), 14 deletions(-) create mode 100644 src/frontend/backend/nx/audio/audio.cpp create mode 100644 src/frontend/backend/nx/audio/audio.hpp diff --git a/src/frontend/backend/nx/CMakeLists.txt b/src/frontend/backend/nx/CMakeLists.txt index 012bb19..656c591 100644 --- a/src/frontend/backend/nx/CMakeLists.txt +++ b/src/frontend/backend/nx/CMakeLists.txt @@ -4,6 +4,8 @@ add_library(backend backend_nx.cpp ftpd_imgui/imgui_deko3d.cpp ftpd_imgui/imgui_nx.cpp + + audio/audio.cpp ) target_link_libraries(backend PRIVATE imgui) diff --git a/src/frontend/backend/nx/audio/audio.cpp b/src/frontend/backend/nx/audio/audio.cpp new file mode 100644 index 0000000..b04d4a5 --- /dev/null +++ b/src/frontend/backend/nx/audio/audio.cpp @@ -0,0 +1,267 @@ +// Copyright 2022 TotalJustice. +// SPDX-License-Identifier: GPL-3.0-only + +// this code is mostly from an old audio player i made +// for the switch, was never released however + +// the "voice" can be created with any sample rate +// and the audio hw handles the resampling to the device output +// this means it can take the 4 sample rates [32768, 65536, 131072, 262144] +// supported by gba, however sampling at anything higher than 65k is taxing. + +// theres no smart handling if too many samples are created. +// if this does happen, samples are dropped. + +// theres very basic time stretching which stretches the +// last sample if there not enough samples, this actually sounds +// very good! + +#include "../../../system.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +// custom allocator for std::vector that respects pool alignment. +template +class PoolAllocator { +public: + using value_type = Type; // used by std::vector + +public: + constexpr PoolAllocator() = default; + constexpr ~PoolAllocator() = default; + + [[nodiscard]] + constexpr auto allocate(const std::size_t n) -> Type* { + return new(align) Type[n]; + } + + constexpr auto deallocate(Type* p, const std::size_t n) noexcept -> void { + delete[] (align, p); + } + +private: + static constexpr std::align_val_t align{0x1000}; +}; + +constexpr AudioRendererConfig cfg = { + .output_rate = AudioRendererOutputRate_48kHz, + .num_voices = 2, + .num_effects = 0, + .num_sinks = 1, + .num_mix_objs = 1, + .num_mix_buffers = 2, +}; + +AudioDriver driver; + +constexpr int voice_id = 0; +constexpr int channels = 2; +constexpr int samples = 4096 * 2; +constexpr int frequency = 65536; +constexpr uint8_t sink_channels[channels]{ 0, 1 }; + +int sink_id; +int mem_pool_id; + +std::vector> mem_pool; +std::size_t spec_size; + +// this is what we write the samples into +std::vector temp_buf; +std::size_t temp_buffer_index; + +// this is what we copy the temp_buf into per audio frame +std::vector wave_buffers; +std::size_t wave_buffer_index; + +std::jthread thread; +std::mutex mutex; + +auto audio_callback(void* user, int16_t left, int16_t right) -> void +{ + std::scoped_lock lock{mutex}; + + if (temp_buffer_index >= temp_buf.size()) + { + return; + } + + temp_buf[temp_buffer_index++] = left; + temp_buf[temp_buffer_index++] = right; +} + +auto audio_thread(std::stop_token token) -> void +{ + for (;;) + { + if (token.stop_requested()) + { + printf("[INFO] stop token requested in loop!\n"); + return; + } + + auto& buffer = wave_buffers[wave_buffer_index]; + if (buffer.state == AudioDriverWaveBufState_Free || buffer.state == AudioDriverWaveBufState_Done) + { + // apparently mem_pool data shouldn't be used directly (opus example). + // so we use a temp buffer then copy in the buffer. + auto data = mem_pool.data() + (wave_buffer_index * spec_size); + { + std::scoped_lock lock{mutex}; + auto data16 = reinterpret_cast(data); + std::ranges::copy(temp_buf, data16); + + // stretch last sample + if (temp_buffer_index >= 2 && temp_buffer_index < temp_buf.size()) + { + for (size_t i = temp_buffer_index; i < temp_buf.size(); i++) + { + data16[i] = temp_buf[temp_buffer_index - 2]; + } + } + + temp_buffer_index = 0; + } + armDCacheFlush(data, spec_size); + + if (!audrvVoiceAddWaveBuf(&driver, voice_id, &buffer)) + { + printf("[ERROR] failed to add wave buffer to voice!\n"); + } + + // resume voice is it was idle or stopped playing. + if (!audrvVoiceIsPlaying(&driver, voice_id)) + { + audrvVoiceStart(&driver, voice_id); + } + + // advance the buffer index + wave_buffer_index = (wave_buffer_index + 1) % wave_buffers.size(); + } + + if (auto r = audrvUpdate(&driver); R_FAILED(r)) + { + printf("[ERROR] failed to update audio driver in loop!\n"); + } + + audrenWaitFrame(); + } +} + +} // namespace + +namespace nx::audio { + +auto init() -> bool +{ + wave_buffers.resize(2); + + if (auto r = audrenInitialize(&cfg); R_FAILED(r)) + { + printf("failed to init audren\n"); + return false; + } + + if (auto r = audrvCreate(&driver, &cfg, channels); R_FAILED(r)) + { + printf("failed to create driver\n"); + return false; + } + + sink_id = audrvDeviceSinkAdd(&driver, AUDREN_DEFAULT_DEVICE_NAME, channels, sink_channels); + + if (auto r = audrvUpdate(&driver); R_FAILED(r)) + { + printf("failed to add sink to driver\n"); + return false; + } + + if (auto r = audrenStartAudioRenderer(); R_FAILED(r)) + { + printf("failed to start audio renderer\n"); + return false; + } + + if (!audrvVoiceInit(&driver, voice_id, channels, PcmFormat_Int16, frequency)) + { + printf("failed to init voice\n"); + return false; + } + + audrvVoiceSetDestinationMix(&driver, voice_id, AUDREN_FINAL_MIX_ID); + if (channels == 1) + { + audrvVoiceSetMixFactor(&driver, voice_id, 1.0F, 0, 0); + audrvVoiceSetMixFactor(&driver, voice_id, 1.0F, 0, 1); + } + else + { + audrvVoiceSetMixFactor(&driver, voice_id, 1.0F, 0, 0); + audrvVoiceSetMixFactor(&driver, voice_id, 0.0F, 0, 1); + audrvVoiceSetMixFactor(&driver, voice_id, 0.0F, 1, 0); + audrvVoiceSetMixFactor(&driver, voice_id, 1.0F, 1, 1); + } + + spec_size = sizeof(std::int16_t) * channels * samples; + const auto mem_pool_size = ((spec_size * wave_buffers.size()) + (AUDREN_MEMPOOL_ALIGNMENT - 1)) &~ (AUDREN_MEMPOOL_ALIGNMENT - 1); + mem_pool.resize(mem_pool_size); + // LOG("unaliged size 0x%lX aligned size: 0x%lX vector size: 0x%lX\n", spec_size * wave_buffers.size(), ((spec_size * wave_buffers.size()) + (AUDREN_MEMPOOL_ALIGNMENT - 1)) &~ (AUDREN_MEMPOOL_ALIGNMENT - 1), mem_pool.size()); + + for (std::size_t i = 0; i < wave_buffers.size(); ++i) { + wave_buffers[i].data_adpcm = mem_pool.data(); + wave_buffers[i].size = mem_pool.size(); + wave_buffers[i].start_sample_offset = i * samples; + wave_buffers[i].end_sample_offset = wave_buffers[i].start_sample_offset + samples; + } + + armDCacheFlush(mem_pool.data(), mem_pool.size()); + + mem_pool_id = audrvMemPoolAdd(&driver, mem_pool.data(), mem_pool.size()); + if (!audrvMemPoolAttach(&driver, mem_pool_id)) + { + printf("[ERROR] failed to attach mem pool!\n"); + return false; + } + + wave_buffer_index = 0; + temp_buf.resize(spec_size / 2); // this is s16 + thread = std::jthread(audio_thread); + sys::System::gameboy_advance.set_audio_callback(audio_callback); + + return true; +} + +auto quit() -> void +{ + // this may take a while to join! + // its probably possible to wait on an audio event + // which if thats the case, then i can manually wake up + // the thread whenever i want. + thread.request_stop(); + thread.join(); + printf("[INFO] joined audio loop thread\n"); + + if (auto r = audrenStopAudioRenderer(); R_FAILED(r)) + { + printf("[ERROR] failed to stop audren!\n"); + } + + audrvVoiceDrop(&driver, voice_id); + audrvClose(&driver); + audrenExit(); + + mem_pool.clear(); +} + +} // namespace nx::audio diff --git a/src/frontend/backend/nx/audio/audio.hpp b/src/frontend/backend/nx/audio/audio.hpp new file mode 100644 index 0000000..e661f29 --- /dev/null +++ b/src/frontend/backend/nx/audio/audio.hpp @@ -0,0 +1,10 @@ +// Copyright 2022 TotalJustice. +// SPDX-License-Identifier: GPL-3.0-only +#pragma once + +namespace nx::audio { + +auto init() -> bool; +auto quit() -> void; + +} // namespace nx::audio diff --git a/src/frontend/backend/nx/backend_nx.cpp b/src/frontend/backend/nx/backend_nx.cpp index 705cb65..2c3609f 100644 --- a/src/frontend/backend/nx/backend_nx.cpp +++ b/src/frontend/backend/nx/backend_nx.cpp @@ -5,6 +5,7 @@ #include "ftpd_imgui/imgui_nx.h" #include "../backend.hpp" #include "../../system.hpp" +#include "audio/audio.hpp" #include #include #include @@ -105,7 +106,7 @@ AppletHookCookie appletHookCookie; auto applet_show_error_message(const char* message, const char* long_message) { ErrorApplicationConfig cfg; - errorApplicationCreate(&cfg, "Unsupported Launch!", "Please launch as application!"); + errorApplicationCreate(&cfg, message, long_message); errorApplicationShow(&cfg); } @@ -431,7 +432,7 @@ auto Texture::quit() -> void if (!imgui::nx::init()) { - applet_show_error_message("Unsupported Launch!", "Please launch as application!"); + applet_show_error_message("Failed to init imgui!", ""); return false; } @@ -454,11 +455,19 @@ auto Texture::quit() -> void padConfigureInput(1, HidNpadStyleSet_NpadStandard); padInitializeDefault(&pad); + if (!nx::audio::init()) + { + applet_show_error_message("failed to open audio!", ""); + return false; + } + return true; } auto quit() -> void { + nx::audio::quit(); + imgui::nx::exit(); // wait for queue to be idle @@ -489,24 +498,30 @@ auto poll_events() -> void } padUpdate(&pad); - const auto down = padGetButtons(&pad); - - System::emu_set_button(gba::A, !!(down & HidNpadButton_A)); - System::emu_set_button(gba::B, !!(down & HidNpadButton_B)); - System::emu_set_button(gba::L, !!(down & HidNpadButton_L)); - System::emu_set_button(gba::R, !!(down & HidNpadButton_R)); - System::emu_set_button(gba::START, !!(down & HidNpadButton_Plus)); - System::emu_set_button(gba::SELECT, !!(down & HidNpadButton_Minus)); - System::emu_set_button(gba::UP, !!(down & HidNpadButton_AnyUp)); - System::emu_set_button(gba::DOWN, !!(down & HidNpadButton_AnyDown)); - System::emu_set_button(gba::LEFT, !!(down & HidNpadButton_AnyLeft)); - System::emu_set_button(gba::RIGHT, !!(down & HidNpadButton_AnyRight)); + const auto buttons = padGetButtons(&pad); + const auto down = padGetButtonsDown(&pad); + + System::emu_set_button(gba::A, !!(buttons & HidNpadButton_A)); + System::emu_set_button(gba::B, !!(buttons & HidNpadButton_B)); + System::emu_set_button(gba::L, !!(buttons & HidNpadButton_L)); + System::emu_set_button(gba::R, !!(buttons & HidNpadButton_R)); + System::emu_set_button(gba::START, !!(buttons & HidNpadButton_Plus)); + System::emu_set_button(gba::SELECT, !!(buttons & HidNpadButton_Minus)); + System::emu_set_button(gba::UP, !!(buttons & HidNpadButton_AnyUp)); + System::emu_set_button(gba::DOWN, !!(buttons & HidNpadButton_AnyDown)); + System::emu_set_button(gba::LEFT, !!(buttons & HidNpadButton_AnyLeft)); + System::emu_set_button(gba::RIGHT, !!(buttons & HidNpadButton_AnyRight)); if (!!(down & HidNpadButton_ZR)) { System::running = false; } + if (!!(down & HidNpadButton_ZL)) + { + System::loadstate(System::rom_path); + } + // this only update inputs and screen size // so it should be called in poll events imgui::nx::newFrame(&pad); From 6ebf96dfb973414663ccc53ed31cd05cc36daf97 Mon Sep 17 00:00:00 2001 From: ITotalJustice <47043333+ITotalJustice@users.noreply.github.com> Date: Fri, 29 Apr 2022 06:53:27 +0100 Subject: [PATCH 3/3] [nx] added basic fs browser and debug monitor see #70 --- README.md | 2 + .../backend/nx => assets/icons}/icon.jpg | Bin assets/icons/icons8-mac-folder-64.png | Bin 0 -> 1001 bytes assets/icons/icons8-visual-game-boy-48.png | Bin 0 -> 1275 bytes .../shaders/nx}/imgui_fsh.glsl | 0 .../shaders/nx}/imgui_vsh.glsl | 0 src/CMakeLists.txt | 3 +- src/frontend/CMakeLists.txt | 24 +- src/frontend/backend/backend.hpp | 4 + src/frontend/backend/nx/CMakeLists.txt | 4 +- src/frontend/backend/nx/audio/audio.cpp | 32 +-- src/frontend/backend/nx/backend_nx.cpp | 221 ++++++++++++++++-- src/frontend/backend/nx/fs.cpp | 177 ++++++++++++++ src/frontend/backend/nx/fs.hpp | 14 ++ src/frontend/backend/sdl2/backend_sdl2.cpp | 4 + src/frontend/system.cpp | 41 +++- src/frontend/system.hpp | 9 +- 17 files changed, 478 insertions(+), 57 deletions(-) rename {src/frontend/backend/nx => assets/icons}/icon.jpg (100%) create mode 100644 assets/icons/icons8-mac-folder-64.png create mode 100644 assets/icons/icons8-visual-game-boy-48.png rename {src/frontend/backend/nx/shaders => assets/shaders/nx}/imgui_fsh.glsl (100%) rename {src/frontend/backend/nx/shaders => assets/shaders/nx}/imgui_vsh.glsl (100%) create mode 100644 src/frontend/backend/nx/fs.cpp create mode 100644 src/frontend/backend/nx/fs.hpp diff --git a/README.md b/README.md index aba2958..ab28e7f 100644 --- a/README.md +++ b/README.md @@ -180,3 +180,5 @@ gba emulator witten in c++23. - ocornut for imgui_club - everyone that has contributed to the bios decomp - [ftpd](https://github.com/mtheall/ftpd) and [nxshell](https://github.com/joel16/NX-Shell) for the deko3d backend for switch. +- Visual Game Boy icon by Icons8 +- Mac Folder icon by Icons8 diff --git a/src/frontend/backend/nx/icon.jpg b/assets/icons/icon.jpg similarity index 100% rename from src/frontend/backend/nx/icon.jpg rename to assets/icons/icon.jpg diff --git a/assets/icons/icons8-mac-folder-64.png b/assets/icons/icons8-mac-folder-64.png new file mode 100644 index 0000000000000000000000000000000000000000..951b4c364494e895cc344c07dc1df1abb897e3bd GIT binary patch literal 1001 zcmVYOS91e%W;c)yf5nI5Vx3xi)Z3^`Ql@BrJ zun1Q*;EQWdu!o!kDd^0M$O6oLOFt3i{RH#xxw2O_C-{Em7z^sap&!6+x8Zar_&x|UUEVK8(iEL0#sbuC)KdaW>g%BHxksP4=LWQQ0??1L{Nh3B zjIA<5CK(3;UIeJEhKd-gcwRO9br*iUP9@MWe}l#r8H0X-E<9S#9gy#$0*wx+e?t}E zopte5_yL?b58XH5-eCNBlAy5r>>>HkG;S=wqNaQS-fhf6R<`sFz~!58=2HG5h|th+ zXmlq;Edd~&{l}Y%a#&IWlYRK{A}DMH9Wo zKaa;vF;)lk{sHaXaQkoQ|2JiY8Btjdv!=n~XCLHY!s>v7Z6nU5p>7PgHvl~Y(9>(z zY;^!Ih_VBZFHd|Wy=jTelGTm+q{8c>d<;yEinZDAs1qb*(M^jaoGCSKfCqnaxQ+L$k&E}Wod-XX|=0z94GYw1%^2f1b@_TA(GoX2~ z#2nxrnaLks6*#To!>G*z%Z=4j74ZfOCLl+&C~n9FX|uaH(U`6wWj$lcw{<#G^ZyY4V~Q~r;4$TAg{3I(t?VwX zXM-W$5bF!JJZp+E7QhQ=1(pK4f2dLQeGad@Du1ReG7H+~>daw}^#tSqRA@EDMy}ho z8r=nakg~q9X4kSSvcoiJZCpP+qpJdCE5brbrziBGbcxXVN{^fzZ!r#s!{Kl^ij)5U X0b4KUTX+TQ00000NkvXXu0mjf9~RX% literal 0 HcmV?d00001 diff --git a/assets/icons/icons8-visual-game-boy-48.png b/assets/icons/icons8-visual-game-boy-48.png new file mode 100644 index 0000000000000000000000000000000000000000..e79581f819aa7229355d18fde16c080575133c3c GIT binary patch literal 1275 zcmV_jiHjz2pSX1 zh7B7-BT3yGjUO0eqH81+fkZbZ1~4QDNDN>CH33UBLJu`g$G3g-|2+TQJ-R!0lFSVUTZXWU~Z;Q05aw5$CAgPjxH`)iDWf5)R^gVIa8 z?+{}$lxX^#$fkfbp{?XB$$$1ryR z$gTf^VBNZugctvMhW_>s7?L{)ee7`sXa%hWnbw~s*7@zyy%iueGd!AfHCqcJX-ll+$!;;sodnpmuXnu=%?dIKWWCS z0{)xo6ITI@)Gd_2$8qDwAfZu*>nDQ|gF~FQxC#g?RvxYNLa%&XQ-G>QKi5wN={V9) z$C3W%n2MIi`o~v5P05p$I5Sh*CKiRKV{LI2P;^=X3)%A!X`8A>KQ|`ZW(o%47S|;K z@{>9s%@b4GMp1gqRkGAiftI6aL9D;qJCFtNyYDC?5~R{zONmoOlIOmpP?TQlDmTn| zEPSjAZ=O__73=HvA+Vw3;8siuuR~;e2(C%`jiA` z+`K>turwW*mkMYP$dZ{_D2GCV^e}w>U99sd>1X)iZ!Uqz)2CISffA>ZDeqm^EE3== z;1fYuWUr*uUO}&Qby^Em+4WrV9ZGm{nL9$Iz3$;ZL#_P^#m+KfHw=Z=*qK(W;koM9 ze=);S2)qypC_JUbbf2~9{@DhE91W1CrQDZJ!ZIEkVoqd`%j)nJN}XB&(#a-A199RJ zcH5=;QG3rZXSz87EoJ-HJEFTl8*n>v5Gi+`+@jCA$sl)mVD2%&2uh+0vyeeHxmrYy zQ$&eYQsgW}+`|Ke1Wu^o(>=GFFSc(te+0{y*1;#HR-R?&hpkRf_WeGr4-_*LYoo! z?90v_$CC+=@@KVW$$>IQ?FLcm6suyH0e&LwH;gcV!wAT2ge$6CQ4>D3w{vIQ l&f?+W;o;%o;o&hm{s)2;$q(oRjz9na002ovPDHLkV1foTVSE4p literal 0 HcmV?d00001 diff --git a/src/frontend/backend/nx/shaders/imgui_fsh.glsl b/assets/shaders/nx/imgui_fsh.glsl similarity index 100% rename from src/frontend/backend/nx/shaders/imgui_fsh.glsl rename to assets/shaders/nx/imgui_fsh.glsl diff --git a/src/frontend/backend/nx/shaders/imgui_vsh.glsl b/assets/shaders/nx/imgui_vsh.glsl similarity index 100% rename from src/frontend/backend/nx/shaders/imgui_vsh.glsl rename to assets/shaders/nx/imgui_vsh.glsl diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3478c1e..b6aee84 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -37,7 +37,8 @@ list(APPEND gcc_flags -Wmissing-requires ) -if (CMAKE_BUILD_TYPE STREQUAL "Debug") +# for switch build, always enable full optimisations, even in debug +if (CMAKE_BUILD_TYPE STREQUAL "Debug" AND NOT NINTENDO_SWITCH) list(APPEND gcc_flags -Og) # always enable otherwise debug builds would be too slow else() list(APPEND gcc_flags -fno-rtti -Ofast) diff --git a/src/frontend/CMakeLists.txt b/src/frontend/CMakeLists.txt index 28511bd..dfdf85c 100644 --- a/src/frontend/CMakeLists.txt +++ b/src/frontend/CMakeLists.txt @@ -30,8 +30,17 @@ FetchContent_Declare(minizip BUILD_COMMAND "" ) +FetchContent_Declare(stb + GIT_REPOSITORY https://github.com/nothings/stb.git + GIT_TAG af1a5bc352164740c1cc1354942b1c6b72eacb8a + GIT_PROGRESS TRUE + CONFIGURE_COMMAND "" + BUILD_COMMAND "" +) + FetchContent_MakeAvailable(imgui) FetchContent_MakeAvailable(imgui_club) +FetchContent_MakeAvailable(stb) add_library(imgui ${imgui_SOURCE_DIR}/imgui.cpp @@ -45,9 +54,12 @@ target_include_directories(imgui PUBLIC ${imgui_SOURCE_DIR}) # memory editor add_library(imgui_club INTERFACE) -target_include_directories(imgui INTERFACE ${imgui_club_SOURCE_DIR}/imgui_memory_editor) +target_include_directories(imgui_club INTERFACE ${imgui_club_SOURCE_DIR}/imgui_memory_editor) # fetch imgui is done! +add_library(stb INTERFACE) +target_include_directories(stb INTERFACE ${stb_SOURCE_DIR}) + # always include backend after fetching imgui add_subdirectory(backend) @@ -134,9 +146,14 @@ target_compile_definitions(main PRIVATE ) if (NINTENDO_SWITCH) + set(ASSETS ${CMAKE_SOURCE_DIR}/assets) + # create romfs folder dkp_add_asset_target(main_romfs ${CMAKE_CURRENT_BINARY_DIR}/romfs) + configure_file(${ASSETS}/icons/icons8-mac-folder-64.png ${CMAKE_CURRENT_BINARY_DIR}/romfs/icons/icons8-mac-folder-64.png COPYONLY) + configure_file(${ASSETS}/icons/icons8-visual-game-boy-48.png ${CMAKE_CURRENT_BINARY_DIR}/romfs/icons/icons8-visual-game-boy-48.png COPYONLY) + # setup nacp nx_generate_nacp(main.nacp NAME "Notorious BEEG" @@ -146,13 +163,13 @@ if (NINTENDO_SWITCH) # create nro (final binary) nx_create_nro(main - ICON ${CMAKE_SOURCE_DIR}/src/frontend/backend/nx/icon.jpg + ICON ${ASSETS}/icons/icon.jpg NACP main.nacp ROMFS main_romfs ) # compile and add shaders to romfs - set(SHADER_FOLDER ${CMAKE_SOURCE_DIR}/src/frontend/backend/nx/shaders) + set(SHADER_FOLDER ${ASSETS}/shaders/nx) nx_add_shader_program(imgui_fsh ${SHADER_FOLDER}/imgui_fsh.glsl frag) nx_add_shader_program(imgui_vsh ${SHADER_FOLDER}/imgui_vsh.glsl vert) @@ -163,4 +180,5 @@ if (NINTENDO_SWITCH) imgui_fsh imgui_vsh ) + endif() diff --git a/src/frontend/backend/backend.hpp b/src/frontend/backend/backend.hpp index ae39fc4..08bcb6c 100644 --- a/src/frontend/backend/backend.hpp +++ b/src/frontend/backend/backend.hpp @@ -13,7 +13,11 @@ namespace sys::backend { auto quit() -> void; auto poll_events() -> void; +// used for setup auto render_begin() -> void; +// render anything specific to the backend +auto render() -> void; +// flip the screen auto render_end() -> void; auto get_texture(TextureID id) -> void*; diff --git a/src/frontend/backend/nx/CMakeLists.txt b/src/frontend/backend/nx/CMakeLists.txt index 656c591..59bb7ec 100644 --- a/src/frontend/backend/nx/CMakeLists.txt +++ b/src/frontend/backend/nx/CMakeLists.txt @@ -6,11 +6,13 @@ add_library(backend ftpd_imgui/imgui_nx.cpp audio/audio.cpp + fs.cpp ) target_link_libraries(backend PRIVATE imgui) target_link_libraries(backend PRIVATE GBA) -target_link_libraries(backend PRIVATE deko3dd) +target_link_libraries(backend PRIVATE deko3d) +target_link_libraries(backend PRIVATE stb) # why is this even needed??? target_include_directories(backend PRIVATE ${DEVKITPRO}/portlibs/switch/include) diff --git a/src/frontend/backend/nx/audio/audio.cpp b/src/frontend/backend/nx/audio/audio.cpp index b04d4a5..8368ea8 100644 --- a/src/frontend/backend/nx/audio/audio.cpp +++ b/src/frontend/backend/nx/audio/audio.cpp @@ -18,12 +18,11 @@ #include "../../../system.hpp" #include -#include -#include #include #include #include #include +#include #include #include #include @@ -74,6 +73,7 @@ constexpr uint8_t sink_channels[channels]{ 0, 1 }; int sink_id; int mem_pool_id; +// mempool that is aligned for audio std::vector> mem_pool; std::size_t spec_size; @@ -82,7 +82,7 @@ std::vector temp_buf; std::size_t temp_buffer_index; // this is what we copy the temp_buf into per audio frame -std::vector wave_buffers; +std::array wave_buffers; std::size_t wave_buffer_index; std::jthread thread; @@ -103,14 +103,8 @@ auto audio_callback(void* user, int16_t left, int16_t right) -> void auto audio_thread(std::stop_token token) -> void { - for (;;) + while (!token.stop_requested()) { - if (token.stop_requested()) - { - printf("[INFO] stop token requested in loop!\n"); - return; - } - auto& buffer = wave_buffers[wave_buffer_index]; if (buffer.state == AudioDriverWaveBufState_Free || buffer.state == AudioDriverWaveBufState_Done) { @@ -125,9 +119,10 @@ auto audio_thread(std::stop_token token) -> void // stretch last sample if (temp_buffer_index >= 2 && temp_buffer_index < temp_buf.size()) { - for (size_t i = temp_buffer_index; i < temp_buf.size(); i++) + for (size_t i = temp_buffer_index; i < temp_buf.size(); i += 2) { - data16[i] = temp_buf[temp_buffer_index - 2]; + data16[i+0] = temp_buf[temp_buffer_index - 2]; // left + data16[i+1] = temp_buf[temp_buffer_index - 1]; // right } } @@ -165,8 +160,6 @@ namespace nx::audio { auto init() -> bool { - wave_buffers.resize(2); - if (auto r = audrenInitialize(&cfg); R_FAILED(r)) { printf("failed to init audren\n"); @@ -216,9 +209,9 @@ auto init() -> bool spec_size = sizeof(std::int16_t) * channels * samples; const auto mem_pool_size = ((spec_size * wave_buffers.size()) + (AUDREN_MEMPOOL_ALIGNMENT - 1)) &~ (AUDREN_MEMPOOL_ALIGNMENT - 1); mem_pool.resize(mem_pool_size); - // LOG("unaliged size 0x%lX aligned size: 0x%lX vector size: 0x%lX\n", spec_size * wave_buffers.size(), ((spec_size * wave_buffers.size()) + (AUDREN_MEMPOOL_ALIGNMENT - 1)) &~ (AUDREN_MEMPOOL_ALIGNMENT - 1), mem_pool.size()); - for (std::size_t i = 0; i < wave_buffers.size(); ++i) { + for (std::size_t i = 0; i < wave_buffers.size(); ++i) + { wave_buffers[i].data_adpcm = mem_pool.data(); wave_buffers[i].size = mem_pool.size(); wave_buffers[i].start_sample_offset = i * samples; @@ -235,8 +228,15 @@ auto init() -> bool } wave_buffer_index = 0; + + // set the buffer size temp_buf.resize(spec_size / 2); // this is s16 + std::ranges::fill(temp_buf, 0); + + // start audio thread thread = std::jthread(audio_thread); + + // set callback for emu sys::System::gameboy_advance.set_audio_callback(audio_callback); return true; diff --git a/src/frontend/backend/nx/backend_nx.cpp b/src/frontend/backend/nx/backend_nx.cpp index 2c3609f..d5290b7 100644 --- a/src/frontend/backend/nx/backend_nx.cpp +++ b/src/frontend/backend/nx/backend_nx.cpp @@ -6,8 +6,13 @@ #include "../backend.hpp" #include "../../system.hpp" #include "audio/audio.hpp" +#include "fs.hpp" + +#define STB_IMAGE_IMPLEMENTATION +#include "stb_image.h" + +#include #include -#include #include #include #include @@ -52,9 +57,11 @@ namespace { struct Texture { - auto init() -> void; + auto init(int w, int h, int bpp, DkImageFormat f, int id, void* data = nullptr) -> void; + auto init(const char* file, int id) -> void; + auto quit() -> void; - auto update(std::uint16_t data[160][240]) -> void; + auto update(void* data) -> void; [[nodiscard]] auto get_image_id() { return image_id; } [[nodiscard]] auto get_sampler_id() { return sampler_id; } @@ -65,18 +72,18 @@ struct Texture // todo: make texture creation like a factory which returns an // id (index into texture array) - static inline const auto image_id = 2; - static inline const auto sampler_id = 1; + int image_id; + int sampler_id; // thse should be set in init() - static inline const auto format = DkImageFormat_RGB5_Unorm; - static inline const auto width = 240; - static inline const auto height = 160; - static inline const auto size = width * height * sizeof(uint16_t); + DkImageFormat format; + int width; + int height; + int size; }; constexpr auto MAX_SAMPLERS = 2; -constexpr auto MAX_IMAGES = 8; +constexpr auto MAX_IMAGES = std::to_underlying(TextureID::max) + 2; constexpr auto FB_NUM = 2u; constexpr auto CMDBUF_SIZE = 1024 * 1024; @@ -98,11 +105,13 @@ dk::ImageDescriptor *s_imageDescriptors = nullptr; dk::UniqueQueue s_queue; dk::UniqueSwapchain s_swapchain; -Texture textures[1]; +Texture textures[std::to_underlying(TextureID::max)]; PadState pad; AppletHookCookie appletHookCookie; +bool show_fs_browser{false}; + auto applet_show_error_message(const char* message, const char* long_message) { ErrorApplicationConfig cfg; @@ -360,8 +369,15 @@ void exit_deko3d(void) { } // SOURCE: https://github.com/joel16/NX-Shell/blob/5a5067afeb6b18c0d2bb4d7b16f71899a768012a/source/textures.cpp#L150 -auto Texture::init() -> void +auto Texture::init(int w, int h, int bpp, DkImageFormat f, int id, void* data) -> void { + width = w; + height = h; + format = f; + image_id = 1 + id; + sampler_id = 1; + size = width * height * bpp; + s_queue.waitIdle(); dk::ImageLayout layout; @@ -379,7 +395,14 @@ auto Texture::init() -> void .setFlags(DkMemBlockFlags_GpuCached | DkMemBlockFlags_Image) .create(); - std::memset(memBlock.getCpuAddr(), 0, size); + if (data != nullptr) + { + std::memcpy(memBlock.getCpuAddr(), data, size); + } + else + { + std::memset(memBlock.getCpuAddr(), 0, size); + } image.initialize(layout, s_imageMemBlock, 0); s_imageDescriptors[image_id].initialize(image); @@ -391,14 +414,38 @@ auto Texture::init() -> void s_queue.submitCommands(s_cmdBuf[0].finishList()); - s_samplerDescriptors[sampler_id].initialize(dk::Sampler{} - .setFilter(DkFilter_Nearest, DkFilter_Nearest) - .setWrapMode(DkWrapMode_ClampToEdge, DkWrapMode_ClampToEdge, DkWrapMode_ClampToEdge)); + // this is a hack because i cba to write good code + static bool sample_already_init = false; + if (!sample_already_init) + { + s_samplerDescriptors[sampler_id].initialize(dk::Sampler{} + .setFilter(DkFilter_Nearest, DkFilter_Nearest) + .setWrapMode(DkWrapMode_ClampToEdge, DkWrapMode_ClampToEdge, DkWrapMode_ClampToEdge)); + } s_queue.waitIdle(); } -auto Texture::update(std::uint16_t data[160][240]) -> void +auto Texture::init(const char* file, int id) -> void +{ + // Load from disk into a raw RGBA buffer + int image_width = 0; + int image_height = 0; + int bpp = 0; + unsigned char* image_data = stbi_load(file, &image_width, &image_height, NULL, 4); + if (image_data == NULL) + { + std::printf("failed to load image: %s\n", file); + return; + } + + init(image_width, image_height, 4, DkImageFormat_RGBA8_Unorm, id, image_data); + stbi_image_free(image_data); + + std::printf("loaded file\n"); +} + +auto Texture::update(void* data) -> void { s_queue.waitIdle(); // is this needed? @@ -442,11 +489,14 @@ auto Texture::quit() -> void // init deko3d for imgui imgui::deko3d::init(s_device, s_queue, s_cmdBuf[0], s_samplerDescriptors[0], s_imageDescriptors[0], dkMakeTextureHandle(0, 0), FB_NUM); - // init all textures being used - for (auto& texture : textures) - { - texture.init(); - } + textures[std::to_underlying(TextureID::emu)].init(240, 160, sizeof(u16), DkImageFormat_RGB5_Unorm, std::to_underlying(TextureID::emu)); + textures[std::to_underlying(TextureID::layer0)].init(240, 160, sizeof(u16), DkImageFormat_RGB5_Unorm, std::to_underlying(TextureID::layer0)); + textures[std::to_underlying(TextureID::layer1)].init(240, 160, sizeof(u16), DkImageFormat_RGB5_Unorm, std::to_underlying(TextureID::layer1)); + textures[std::to_underlying(TextureID::layer2)].init(240, 160, sizeof(u16), DkImageFormat_RGB5_Unorm, std::to_underlying(TextureID::layer2)); + textures[std::to_underlying(TextureID::layer3)].init(240, 160, sizeof(u16), DkImageFormat_RGB5_Unorm, std::to_underlying(TextureID::layer3)); + + textures[std::to_underlying(TextureID::folder_icon)].init("romfs:/icons/icons8-mac-folder-64.png", std::to_underlying(TextureID::folder_icon)); + textures[std::to_underlying(TextureID::file_icon)].init("romfs:/icons/icons8-visual-game-boy-48.png", std::to_underlying(TextureID::file_icon)); // setup callback for applet events appletHook(&appletHookCookie, appplet_hook_calback, nullptr); @@ -522,14 +572,122 @@ auto poll_events() -> void System::loadstate(System::rom_path); } + if (!!(down & HidNpadButton_Y)) + { + show_fs_browser ^= 1; + if (System::has_rom) + { + // if showing browser, stop running, else run + System::emu_run = !show_fs_browser; + } + } + // this only update inputs and screen size // so it should be called in poll events imgui::nx::newFrame(&pad); } +void show_debug_monitor() +{ + static int corner = -1; + ImGuiIO& io = ImGui::GetIO(); + ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav; + if (corner != -1) + { + const float PAD = 10.0f; + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImVec2 work_pos = viewport->WorkPos; // Use work area to avoid menu-bar/task-bar, if any! + ImVec2 work_size = viewport->WorkSize; + ImVec2 window_pos, window_pos_pivot; + window_pos.x = (corner & 1) ? (work_pos.x + work_size.x - PAD) : (work_pos.x + PAD); + window_pos.y = (corner & 2) ? (work_pos.y + work_size.y - PAD) : (work_pos.y + PAD); + window_pos_pivot.x = (corner & 1) ? 1.0f : 0.0f; + window_pos_pivot.y = (corner & 2) ? 1.0f : 0.0f; + ImGui::SetNextWindowPos(window_pos, ImGuiCond_Always, window_pos_pivot); + window_flags |= ImGuiWindowFlags_NoMove; + } + + ImGui::SetNextWindowBgAlpha(0.50f); // Transparent background + if (ImGui::Begin("NX overlay", nullptr, window_flags)) + { + ImGui::Text("BEEG Debug Monitor\n"); + ImGui::Separator(); + if (ImGui::CollapsingHeader("Memory")) + { + // SOURCE: https://switchbrew.org/wiki/SVC#SystemInfoType + constexpr u64 TotalPhysicalMemorySize_Application = 0; + constexpr u64 TotalPhysicalMemorySize_Applet = 1; + constexpr u64 TotalPhysicalMemorySize_System = 2; + constexpr u64 TotalPhysicalMemorySize_SystemUnsafe = 3; + + constexpr u64 UsedPhysicalMemorySize_Application = 0; + constexpr u64 UsedPhysicalMemorySize_Applet = 1; + constexpr u64 UsedPhysicalMemorySize_System = 2; + constexpr u64 UsedPhysicalMemorySize_SystemUnsafe = 3; + + constexpr u8 svcGetSystemInfo_id = 0x6F; + + // check if we have priv to use the sys call! + if (envIsSyscallHinted(svcGetSystemInfo_id)) + { + u64 total_application = 0; + u64 total_applet = 0; + u64 total_system = 0; + u64 total_unsafe = 0; + + u64 used_application = 0; + u64 used_applet = 0; + u64 used_system = 0; + u64 used_unsafe = 0; + + svcGetSystemInfo(&total_application, 0, INVALID_HANDLE, TotalPhysicalMemorySize_Application); + svcGetSystemInfo(&total_applet, 0, INVALID_HANDLE, TotalPhysicalMemorySize_Applet); + svcGetSystemInfo(&total_system, 0, INVALID_HANDLE, TotalPhysicalMemorySize_System); + svcGetSystemInfo(&total_unsafe, 0, INVALID_HANDLE, TotalPhysicalMemorySize_SystemUnsafe); + + svcGetSystemInfo(&used_application, 1, INVALID_HANDLE, UsedPhysicalMemorySize_Application); + svcGetSystemInfo(&used_applet, 1, INVALID_HANDLE, UsedPhysicalMemorySize_Applet); + svcGetSystemInfo(&used_system, 1, INVALID_HANDLE, UsedPhysicalMemorySize_System); + svcGetSystemInfo(&used_unsafe, 1, INVALID_HANDLE, UsedPhysicalMemorySize_SystemUnsafe); + + ImGui::Text("[Application] %.2f MB\t%.2f MB\n", (double)used_application / 1024.0 / 1024.0, (double)total_application / 1024.0 / 1024.0); + ImGui::Text("[Applet] %.2f MB\t%.2f MB\n", (double)used_applet / 1024.0 / 1024.0, (double)total_applet / 1024.0 / 1024.0); + ImGui::Text("[System] %.2f MB\t%.2f MB\n", (double)used_system / 1024.0 / 1024.0, (double)total_system / 1024.0 / 1024.0); + ImGui::Text("[SystemUnsafe] %.2f MB\t%.2f MB\n", (double)used_unsafe / 1024.0 / 1024.0, (double)total_unsafe / 1024.0 / 1024.0); + } + } + + if (ImGui::CollapsingHeader("Audio")) + { + + } + + if (ImGui::CollapsingHeader("Display")) + { + + } + + if (ImGui::CollapsingHeader("Misc")) + { + + } + } + ImGui::End(); +} + auto render_begin() -> void { - // imgui::nx::newFrame(&pad); +} + +auto render() -> void +{ + show_debug_monitor(); + + if (!sys::System::has_rom || show_fs_browser) + { + // fs returns true when a rom has been loaded + show_fs_browser = !nx::fs::render(); + } } auto render_end() -> void @@ -568,14 +726,12 @@ auto render_end() -> void auto get_texture(TextureID id) -> void* { - assert(id == TextureID::emu && "only emu texture is impl!"); auto& texture = textures[std::to_underlying(id)]; return imgui::deko3d::makeTextureID(dkMakeTextureHandle(texture.get_image_id(), texture.get_sampler_id())); } auto update_texture(TextureID id, std::uint16_t pixels[160][240]) -> void { - assert(id == TextureID::emu && "only emu texture is impl!"); textures[std::to_underlying(id)].update(pixels); } @@ -601,6 +757,21 @@ auto toggle_fullscreen() -> void auto open_url(const char* url) -> void { + WebCommonConfig config{}; + + auto rc = webPageCreate(&config, url); + + if (R_SUCCEEDED(rc)) + { + rc = webConfigSetWhitelist(&config, "^http*"); + if (R_SUCCEEDED(rc)) + { + rc = webConfigShow(&config, nullptr); + if (R_SUCCEEDED(rc)) + { + } + } + } } } // sys::backend diff --git a/src/frontend/backend/nx/fs.cpp b/src/frontend/backend/nx/fs.cpp new file mode 100644 index 0000000..eb729b5 --- /dev/null +++ b/src/frontend/backend/nx/fs.cpp @@ -0,0 +1,177 @@ +// Copyright 2022 TotalJustice. +// SPDX-License-Identifier: GPL-3.0-only + +#include "fs.hpp" +#include "../backend.hpp" +#include "../../system.hpp" +#include "imgui_internal.h" +#include +#include +#include +#include + +namespace nx::fs { +namespace { + +struct Entry +{ + std::filesystem::path path; + std::string filename; + bool is_dir; +}; + +std::filesystem::path current_path; +std::vector entries; +bool in_new_dir{true}; + +auto scan() -> void +{ + entries.clear(); + + if (current_path.empty()) + { + #if defined(__SWITCH__) + current_path = "/"; + #else + current_path = std::filesystem::current_path(); + #endif + } + + // walk up dir + if (current_path.has_parent_path()) + { + entries.emplace_back(current_path.parent_path(), "../", true); + } + + // should probably have a max here because uhhh + // a folder having 10000+ entries would suck super bad + for (const auto& entry : std::filesystem::directory_iterator{current_path}) + { + const auto& path = entry.path(); + const auto filename = path.filename().string(); + + if (filename.starts_with('.')) + { + continue; + } + + if (entry.is_directory()) + { + entries.emplace_back(path, filename, true); + } + else if (entry.is_regular_file()) + { + if (!path.has_extension()) + { + continue; + } + + const auto extension = path.extension(); + + if (extension != ".gba" && extension != ".zip") + { + continue; + } + + entries.emplace_back(path, filename, false); + } + } +} + +} // namespace + +auto render() -> bool +{ + // ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings; + const auto window_flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse; + + // fill the screen (as this fs is intended for consoles!) + const auto [w, h] = sys::backend::get_window_size(); + ImGui::SetNextWindowPos(ImVec2(0, 0)); + ImGui::SetNextWindowSize(ImVec2(w, h)); + + if (ImGui::Begin("idk what this thing is", nullptr, window_flags)) + { + // set title + ImGui::Text("Path: %s\n", (current_path).string().c_str()); + ImGui::Spacing(); + + const auto table_flags = ImGuiTableFlags_Resizable | ImGuiTableFlags_Sortable | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_ScrollY; + + // basically i dont want the Begin() to have focus, i want the table + // to alwways be focused. + ImGui::SetNextWindowFocus(); + if (ImGui::BeginTable("##table fs", 1, table_flags)) + { + // this stops back button being spammed + static bool back_pressed = false; + + if (ImGui::IsNavInputDown(ImGuiNavInput_Cancel)) + { + if (!back_pressed) + { + ImGui::GetIO().NavInputs[ImGuiNavInput_Cancel] = 0; + + if (current_path.has_parent_path()) + { + printf("has parent path\n"); + current_path = current_path.parent_path(); + in_new_dir = true; + } + + back_pressed = true; + } + } + else + { + back_pressed = false; + } + + // if we are in a new directory, scan the entries + if (in_new_dir) + { + scan(); + in_new_dir = false; + } + + const auto folder_icon = sys::backend::get_texture(sys::TextureID::folder_icon); + const auto file_icon = sys::backend::get_texture(sys::TextureID::file_icon); + + for (const auto& entry : entries) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + + const auto texture = entry.is_dir ? folder_icon : file_icon; + if (texture != nullptr) + { + ImGui::Image(texture, ImVec2(75, 50)); + } + + ImGui::SameLine(); + + if (ImGui::Selectable(entry.filename.c_str(), false, ImGuiSelectableFlags_SpanAllColumns, ImVec2(0, 50))) + { + if (entry.is_dir) + { + current_path = entry.path; + in_new_dir = true; + } + else if (sys::System::loadrom(entry.path.string())) + { + ImGui::EndTable(); + ImGui::End(); + return true; + } + } + } + + ImGui::EndTable(); + } + } + ImGui::End(); + // display the entries + return false; +} + +} // namespace nx::fs diff --git a/src/frontend/backend/nx/fs.hpp b/src/frontend/backend/nx/fs.hpp new file mode 100644 index 0000000..ce2cb42 --- /dev/null +++ b/src/frontend/backend/nx/fs.hpp @@ -0,0 +1,14 @@ +// Copyright 2022 TotalJustice. +// SPDX-License-Identifier: GPL-3.0-only + +#include + +// this should probably be a generic impl for all +// systems that don't have os browser +// for now, switch specific +namespace nx::fs { + +// returns true if a file is selected +auto render() -> bool; + +} // namespace nx::fs diff --git a/src/frontend/backend/sdl2/backend_sdl2.cpp b/src/frontend/backend/sdl2/backend_sdl2.cpp index ae93c7c..c8ffaaa 100644 --- a/src/frontend/backend/sdl2/backend_sdl2.cpp +++ b/src/frontend/backend/sdl2/backend_sdl2.cpp @@ -528,6 +528,10 @@ auto render_begin() -> void ImGui_ImplSDL2_NewFrame(); } +auto render() -> void +{ +} + auto render_end() -> void { SDL_RenderClear(renderer); diff --git a/src/frontend/system.cpp b/src/frontend/system.cpp index 5ee5b4f..c2da68a 100644 --- a/src/frontend/system.cpp +++ b/src/frontend/system.cpp @@ -326,11 +326,6 @@ auto System::emu_set_button(gba::Button button, bool down) -> void auto System::init(int argc, char** argv) -> bool { #if defined(__SWITCH__) - if (!System::loadrom("/roms/gba/doom.gba")) - { - std::printf("failed to loadrom\n"); - return false; - } #else if (argc < 2) { @@ -660,7 +655,7 @@ auto System::emu_update_texture() -> void auto System::emu_render() -> void { const auto flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoBringToFrontOnFocus; - ImGui::SetNextWindowPos(ImVec2(0, emu_rect.y)); + ImGui::SetNextWindowPos(ImVec2(emu_rect.x, emu_rect.y)); ImGui::SetNextWindowSize(ImVec2(emu_rect.w, emu_rect.h)); ImGui::SetNextWindowSizeConstraints({0, 0}, ImVec2(emu_rect.w, emu_rect.h)); @@ -721,6 +716,8 @@ auto System::run_render() -> void resize_to_menubar(); + backend::render(); + // Rendering (REMEMBER TO RENDER IMGUI STUFF [BEFORE] THIS LINE) ImGui::Render(); backend::render_end(); @@ -774,15 +771,39 @@ auto System::resize_to_menubar() -> void System::resize_emu_screen(); } +int get_scale(int w, int h) +{ + const auto scale_w = w / 240; + const auto scale_h = h / 160; + + return std::min(scale_w, scale_h); +} + auto System::resize_emu_screen() -> void { const auto [w, h] = backend::get_window_size(); // update rect - emu_rect.x = 0; - emu_rect.y = menubar_height; - emu_rect.w = w; - emu_rect.h = h-menubar_height; + if (emu_stretch) + { + emu_rect.x = 0; + emu_rect.y = menubar_height; + emu_rect.w = w; + emu_rect.h = h-menubar_height; + } + else + { + const auto min_scale = get_scale(w, h); + + emu_rect.w = 240 * min_scale; + emu_rect.h = 160 * min_scale; + emu_rect.x = (w - emu_rect.w) / 2; + emu_rect.y = (h - emu_rect.h) / 2; + // emu_rect.x = 0; + // emu_rect.y = menubar_height; + // emu_rect.w = w; + // emu_rect.h = h-menubar_height; + } } } // namespace sys diff --git a/src/frontend/system.hpp b/src/frontend/system.hpp index a9154b7..9b6aad9 100644 --- a/src/frontend/system.hpp +++ b/src/frontend/system.hpp @@ -20,6 +20,10 @@ enum class TextureID layer1, layer2, layer3, + folder_icon, + file_icon, + + max, // not real texture }; struct Rect @@ -61,7 +65,7 @@ struct System static inline std::string rom_path{}; static inline bool has_rom{false}; static inline bool running{true}; - static inline bool emu_run{true}; + static inline bool emu_run{false}; static inline bool show_debug_window{false}; static inline bool show_demo_window{false}; static inline bool show_menubar{true}; @@ -74,6 +78,9 @@ struct System static inline auto start_time = std::chrono::high_resolution_clock::now(); #endif + // set to true to fill the screen + static inline bool emu_stretch{false}; + struct Layer { Layer(TextureID i) : id{i} {};