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 0000000..7df95de Binary files /dev/null and b/src/frontend/backend/nx/icon.jpg differ diff --git a/src/frontend/backend/nx/shaders/imgui_fsh.glsl b/src/frontend/backend/nx/shaders/imgui_fsh.glsl new file mode 100644 index 0000000..8908ca4 --- /dev/null +++ b/src/frontend/backend/nx/shaders/imgui_fsh.glsl @@ -0,0 +1,48 @@ +// 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. + +#version 460 + +layout (location = 0) in vec2 vtxUv; +layout (location = 1) in vec4 vtxColor; + +layout (binding = 0) uniform sampler2D tex; + +layout (std140, binding = 0) uniform FragUBO { + uint font; +} ubo; + +layout (location = 0) out vec4 outColor; + +void main() +{ + // font texture is single-channel (alpha) + if (ubo.font != 0) + outColor = vtxColor * vec4 (vec3 (1.0), texture (tex, vtxUv).r); + else + outColor = vtxColor * texture (tex, vtxUv); +} diff --git a/src/frontend/backend/nx/shaders/imgui_vsh.glsl b/src/frontend/backend/nx/shaders/imgui_vsh.glsl new file mode 100644 index 0000000..5aebf24 --- /dev/null +++ b/src/frontend/backend/nx/shaders/imgui_vsh.glsl @@ -0,0 +1,47 @@ +// 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. + +#version 460 + +layout (location = 0) in vec2 inPos; +layout (location = 1) in vec2 inUv; +layout (location = 2) in vec4 inColor; + +layout (location = 0) out vec2 vtxUv; +layout (location = 1) out vec4 vtxColor; + +layout (std140, binding = 0) uniform VertUBO +{ + mat4 projMtx; +} ubo; + +void main() +{ + gl_Position = ubo.projMtx * vec4 (inPos, 0.0, 1.0); + vtxUv = inUv; + vtxColor = inColor; +} diff --git a/src/frontend/backend/sdl2/CMakeLists.txt b/src/frontend/backend/sdl2/CMakeLists.txt index 8fdcb3f..74084a0 100644 --- a/src/frontend/backend/sdl2/CMakeLists.txt +++ b/src/frontend/backend/sdl2/CMakeLists.txt @@ -10,10 +10,6 @@ target_include_directories(backend PRIVATE ${imgui_SOURCE_DIR}/backends ) -target_include_directories(backend PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR} -) - find_package(SDL2 CONFIG REQUIRED) target_link_libraries(backend PRIVATE imgui) diff --git a/src/frontend/backend/sdl2/backend_sdl2.cpp b/src/frontend/backend/sdl2/backend_sdl2.cpp index 93a0aa6..ae93c7c 100644 --- a/src/frontend/backend/sdl2/backend_sdl2.cpp +++ b/src/frontend/backend/sdl2/backend_sdl2.cpp @@ -1,7 +1,7 @@ // Copyright 2022 TotalJustice. // SPDX-License-Identifier: GPL-3.0-only -#include "backend_sdl2.hpp" +#include "../backend.hpp" #include "../../system.hpp" #include "../../icon.hpp" #include @@ -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;