From 34c6d6121532c227bb0a27c4c50ff8eee8da71a5 Mon Sep 17 00:00:00 2001 From: Michael Theall Date: Mon, 6 Apr 2020 00:36:03 -0500 Subject: [PATCH] Documentation and minor refactors --- .gitignore | 12 +- {gfx.3ds => 3ds/gfx}/battery0.png | Bin {gfx.3ds => 3ds/gfx}/battery1.png | Bin {gfx.3ds => 3ds/gfx}/battery2.png | Bin {gfx.3ds => 3ds/gfx}/battery3.png | Bin {gfx.3ds => 3ds/gfx}/battery4.png | Bin {gfx.3ds => 3ds/gfx}/batteryCharge.png | Bin {gfx.3ds => 3ds/gfx}/c3dlogo.png | Bin {gfx.3ds => 3ds/gfx}/gfx.t3s | 0 {gfx.3ds => 3ds/gfx}/wifi1.png | Bin {gfx.3ds => 3ds/gfx}/wifi2.png | Bin {gfx.3ds => 3ds/gfx}/wifi3.png | Bin {gfx.3ds => 3ds/gfx}/wifiNull.png | Bin Makefile | 26 +- Makefile.3ds | 17 +- Makefile.linux | 10 +- Makefile.switch | 17 +- include/fs.h | 74 +++++- include/ftpServer.h | 25 +- include/ftpSession.h | 243 +++++++++++++++++- include/ioBuffer.h | 41 ++- include/log.h | 38 +++ include/platform.h | 43 +++- include/sockAddr.h | 34 +++ include/socket.h | 78 +++++- source/3ds/imgui_citro3d.cpp | 141 +++++++++-- source/3ds/imgui_citro3d.h | 4 + source/3ds/imgui_ctru.cpp | 45 +++- source/3ds/imgui_ctru.h | 3 + source/3ds/platform.cpp | 59 ++++- source/3ds/vshader.v.pica | 16 +- source/fs.cpp | 9 +- source/ftpServer.cpp | 16 +- source/ftpSession.cpp | 24 ++ source/ioBuffer.cpp | 1 + source/{pc => linux}/KHR/khrplatform.h | 0 source/{pc => linux}/glad.c | 0 source/{pc => linux}/glad/glad.h | 0 source/{pc => linux}/imgui_impl_glfw.cpp | 0 source/{pc => linux}/imgui_impl_glfw.h | 0 source/{pc => linux}/imgui_impl_opengl3.cpp | 0 source/{pc => linux}/imgui_impl_opengl3.h | 0 source/{pc => linux}/platform.cpp | 35 ++- source/log.cpp | 26 +- source/main.cpp | 11 +- source/sockAddr.cpp | 19 +- source/socket.cpp | 10 +- source/{nx => switch}/imgui_deko3d.cpp | 262 +++++++++++++++----- source/{nx => switch}/imgui_deko3d.h | 0 source/{nx => switch}/imgui_fsh.glsl | 1 + source/{nx => switch}/imgui_nx.cpp | 223 +++++++++++------ source/{nx => switch}/imgui_nx.h | 0 source/{nx => switch}/imgui_vsh.glsl | 0 source/{nx => switch}/init.c | 5 + source/{nx => switch}/platform.cpp | 16 +- {gfx.switch => switch/gfx}/deko3d.png | Bin 56 files changed, 1294 insertions(+), 290 deletions(-) rename {gfx.3ds => 3ds/gfx}/battery0.png (100%) rename {gfx.3ds => 3ds/gfx}/battery1.png (100%) rename {gfx.3ds => 3ds/gfx}/battery2.png (100%) rename {gfx.3ds => 3ds/gfx}/battery3.png (100%) rename {gfx.3ds => 3ds/gfx}/battery4.png (100%) rename {gfx.3ds => 3ds/gfx}/batteryCharge.png (100%) rename {gfx.3ds => 3ds/gfx}/c3dlogo.png (100%) rename {gfx.3ds => 3ds/gfx}/gfx.t3s (100%) rename {gfx.3ds => 3ds/gfx}/wifi1.png (100%) rename {gfx.3ds => 3ds/gfx}/wifi2.png (100%) rename {gfx.3ds => 3ds/gfx}/wifi3.png (100%) rename {gfx.3ds => 3ds/gfx}/wifiNull.png (100%) rename source/{pc => linux}/KHR/khrplatform.h (100%) rename source/{pc => linux}/glad.c (100%) rename source/{pc => linux}/glad/glad.h (100%) rename source/{pc => linux}/imgui_impl_glfw.cpp (100%) rename source/{pc => linux}/imgui_impl_glfw.h (100%) rename source/{pc => linux}/imgui_impl_opengl3.cpp (100%) rename source/{pc => linux}/imgui_impl_opengl3.h (100%) rename source/{pc => linux}/platform.cpp (87%) rename source/{nx => switch}/imgui_deko3d.cpp (72%) rename source/{nx => switch}/imgui_deko3d.h (100%) rename source/{nx => switch}/imgui_fsh.glsl (96%) rename source/{nx => switch}/imgui_nx.cpp (95%) rename source/{nx => switch}/imgui_nx.h (100%) rename source/{nx => switch}/imgui_vsh.glsl (100%) rename source/{nx => switch}/init.c (91%) rename source/{nx => switch}/platform.cpp (93%) rename {gfx.switch => switch/gfx}/deko3d.png (100%) diff --git a/.gitignore b/.gitignore index a46b511..01f69b5 100644 --- a/.gitignore +++ b/.gitignore @@ -10,8 +10,10 @@ *.pfs0 *.smdh .gdb_history -build.*/ -ftpd -romfs.3ds/*.t3x -romfs.switch/*.zst -romfs.switch/shaders/*.dksh +3ds/build +3ds/romfs/*.t3x +linux/build +linux/ftpd +switch/build +switch/romfs/*.zst +switch/romfs/shaders/*.dksh diff --git a/gfx.3ds/battery0.png b/3ds/gfx/battery0.png similarity index 100% rename from gfx.3ds/battery0.png rename to 3ds/gfx/battery0.png diff --git a/gfx.3ds/battery1.png b/3ds/gfx/battery1.png similarity index 100% rename from gfx.3ds/battery1.png rename to 3ds/gfx/battery1.png diff --git a/gfx.3ds/battery2.png b/3ds/gfx/battery2.png similarity index 100% rename from gfx.3ds/battery2.png rename to 3ds/gfx/battery2.png diff --git a/gfx.3ds/battery3.png b/3ds/gfx/battery3.png similarity index 100% rename from gfx.3ds/battery3.png rename to 3ds/gfx/battery3.png diff --git a/gfx.3ds/battery4.png b/3ds/gfx/battery4.png similarity index 100% rename from gfx.3ds/battery4.png rename to 3ds/gfx/battery4.png diff --git a/gfx.3ds/batteryCharge.png b/3ds/gfx/batteryCharge.png similarity index 100% rename from gfx.3ds/batteryCharge.png rename to 3ds/gfx/batteryCharge.png diff --git a/gfx.3ds/c3dlogo.png b/3ds/gfx/c3dlogo.png similarity index 100% rename from gfx.3ds/c3dlogo.png rename to 3ds/gfx/c3dlogo.png diff --git a/gfx.3ds/gfx.t3s b/3ds/gfx/gfx.t3s similarity index 100% rename from gfx.3ds/gfx.t3s rename to 3ds/gfx/gfx.t3s diff --git a/gfx.3ds/wifi1.png b/3ds/gfx/wifi1.png similarity index 100% rename from gfx.3ds/wifi1.png rename to 3ds/gfx/wifi1.png diff --git a/gfx.3ds/wifi2.png b/3ds/gfx/wifi2.png similarity index 100% rename from gfx.3ds/wifi2.png rename to 3ds/gfx/wifi2.png diff --git a/gfx.3ds/wifi3.png b/3ds/gfx/wifi3.png similarity index 100% rename from gfx.3ds/wifi3.png rename to 3ds/gfx/wifi3.png diff --git a/gfx.3ds/wifiNull.png b/3ds/gfx/wifiNull.png similarity index 100% rename from gfx.3ds/wifiNull.png rename to 3ds/gfx/wifiNull.png diff --git a/Makefile b/Makefile index 2feff58..9b4213f 100644 --- a/Makefile +++ b/Makefile @@ -17,19 +17,19 @@ all: 3dsx nro linux nxlink: @$(MAKE) -f Makefile.switch nxlink -3dslink: 3dsx - @/opt/devkitpro/tools/bin/3dslink $(TARGET)-3ds.3dsx +3dslink: + @$(MAKE) -f Makefile.3ds 3dslink format: @clang-format -style=file -i $(filter-out \ include/imgui.h \ - source/pc/imgui_impl_glfw.cpp \ - source/pc/imgui_impl_glfw.h \ - source/pc/imgui_impl_opengl3.cpp \ - source/pc/imgui_impl_opengl3.h \ - source/pc/KHR/khrplatform.h \ - source/pc/glad.c \ - source/pc/glad/glad.h \ + source/linux/imgui_impl_glfw.cpp \ + source/linux/imgui_impl_glfw.h \ + source/linux/imgui_impl_opengl3.cpp \ + source/linux/imgui_impl_opengl3.h \ + source/linux/KHR/khrplatform.h \ + source/linux/glad.c \ + source/linux/glad/glad.h \ source/imgui/imgui.cpp \ source/imgui/imgui_demo.cpp \ source/imgui/imgui_draw.cpp \ @@ -41,9 +41,9 @@ format: $(shell find source include -type f -name \*.c -o -name \*.cpp -o -name \*.h)) release: release-3ds release-nro - @xz -c <$(TARGET)-3ds.3dsx >ftpd.3dsx.xz - @echo xz -c <$(TARGET)-3ds.cia >ftpd.cia.xz - @echo xz -c <$(TARGET)-nx.nro >ftpd.nro.xz + @xz -c <3ds/$(TARGET).3dsx >ftpd.3dsx.xz + @xz -c <3ds/$(TARGET).cia >ftpd.cia.xz + @xz -c ftpd.nro.xz nro: @$(MAKE) -f Makefile.switch all @@ -64,7 +64,7 @@ release-cia: release-3dsx @$(MAKE) DEFINES=-NDEBUG -f Makefile.3ds cia release-3ds: - # can't let these three run in parallel with each other due to using same + # can't let these run in parallel with each other due to using same # .elf file name @$(MAKE) DEFINES=-DNDEBUG -f Makefile.3ds 3dsx @$(MAKE) DEFINES=-DNDEBUG -f Makefile.3ds cia diff --git a/Makefile.3ds b/Makefile.3ds index 4603a50..179a4b7 100644 --- a/Makefile.3ds +++ b/Makefile.3ds @@ -31,13 +31,13 @@ include $(DEVKITARM)/3ds_rules # - icon.png # - /default_icon.png #--------------------------------------------------------------------------------- -TARGET := $(notdir $(CURDIR))-3ds -BUILD := build.3ds +TARGET := 3ds/$(notdir $(CURDIR)) +BUILD := 3ds/build SOURCES := source source/3ds source/imgui DATA := data INCLUDES := include -GRAPHICS := gfx.3ds -ROMFS := romfs.3ds +GRAPHICS := 3ds/gfx +ROMFS := 3ds/romfs GFXBUILD := $(ROMFS) APP_TITLE := ftpd @@ -66,7 +66,7 @@ CFLAGS += $(INCLUDE) -DARM11 -D_3DS \ CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=gnu++17 ASFLAGS := -g $(ARCH) -LDFLAGS = -specs=3dsx.specs -g $(ARCH) -Wl,-Map,$(TARGET).map $(OPTIMIZE) +LDFLAGS = -specs=3dsx.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) $(OPTIMIZE) LIBS := -lcitro3d -lctru -lm @@ -81,7 +81,7 @@ LIBDIRS := $(CTRULIB) # no real need to edit anything past this point unless you need to add additional # rules for different file extensions #--------------------------------------------------------------------------------- -ifneq ($(BUILD),$(notdir $(CURDIR))) +ifneq ($(TOPDIR)/$(BUILD),$(CURDIR)) #--------------------------------------------------------------------------------- export OUTPUT := $(CURDIR)/$(TARGET) @@ -165,7 +165,7 @@ ifneq ($(ROMFS),) export _3DSXFLAGS += --romfs=$(CURDIR)/$(ROMFS) endif -.PHONY: $(BUILD) clean all 3dsx cia +.PHONY: $(BUILD) clean all 3dsx cia 3dslink #--------------------------------------------------------------------------------- all: $(BUILD) $(GFXBUILD) $(DEPSDIR) $(ROMFS_T3XFILES) $(T3XHFILES) @@ -196,6 +196,9 @@ $(GFXBUILD)/%.t3x $(BUILD)/%.h: %.t3s cia: $(BUILD) $(GFXBUILD) $(DEPSDIR) $(ROMFS_T3XFILES) $(T3XHFILES) @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile.3ds cia +3dslink: 3dsx + @3dslink $(OUTPUT).3dsx + #--------------------------------------------------------------------------------- clean: @echo clean ... diff --git a/Makefile.linux b/Makefile.linux index a8c02f0..f76a6a7 100644 --- a/Makefile.linux +++ b/Makefile.linux @@ -1,12 +1,12 @@ -TARGET := ftpd -BUILD := build.linux +TARGET := linux/ftpd +BUILD := linux/build -CFILES := $(wildcard source/pc/*.c) +CFILES := $(wildcard source/linux/*.c) OFILES := $(patsubst source/%,$(BUILD)/%,$(CFILES:.c=.c.o)) -CXXFILES := $(wildcard source/*.cpp source/imgui/*.cpp source/pc/*.cpp) +CXXFILES := $(wildcard source/*.cpp source/imgui/*.cpp source/linux/*.cpp) OXXFILES := $(patsubst source/%,$(BUILD)/%,$(CXXFILES:.cpp=.cpp.o)) -CPPFLAGS := -g -Wall -pthread -Iinclude -Isource/pc \ +CPPFLAGS := -g -Wall -pthread -Iinclude -Isource/linux \ `pkg-config --cflags gl glfw3` \ -DSTATUS_STRING="\"ftpd v$(VERSION)\"" \ -DIMGUI_DISABLE_INCLUDE_IMCONFIG_H=1 \ diff --git a/Makefile.switch b/Makefile.switch index 03329a6..f71801e 100644 --- a/Makefile.switch +++ b/Makefile.switch @@ -42,13 +42,13 @@ APP_AUTHOR := mtheall, TuxSH, WinterMute ICON := meta/ftpd.jpg APP_VERSION := $(VERSION) -TARGET := $(notdir $(CURDIR))-nx -BUILD := build.switch -SOURCES := source source/imgui source/nx +TARGET := switch/$(notdir $(CURDIR)) +BUILD := switch/build +SOURCES := source source/imgui source/switch DATA := data INCLUDES := include -GRAPHICS := gfx.switch -ROMFS := romfs.switch +GRAPHICS := switch/gfx +ROMFS := switch/romfs # Output folders for autogenerated files in romfs OUT_SHADERS := shaders @@ -85,7 +85,7 @@ LIBDIRS := $(PORTLIBS) $(LIBNX) # no real need to edit anything past this point unless you need to add additional # rules for different file extensions #--------------------------------------------------------------------------------- -ifneq ($(BUILD),$(notdir $(CURDIR))) +ifneq ($(TOPDIR)/$(BUILD),$(CURDIR)) #--------------------------------------------------------------------------------- export OUTPUT := $(CURDIR)/$(TARGET) @@ -193,7 +193,7 @@ all: $(ROMFS_TARGETS) | $(BUILD) @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile.switch nxlink: all - @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile.switch nxlink + @nxlink -s $(OUTPUT).nro $(BUILD): @mkdir -p $@ @@ -260,9 +260,6 @@ ifeq ($(strip $(APP_JSON)),) all : $(OUTPUT).nro -nxlink: $(OUTPUT).nro - @nxlink -s $(OUTPUT).nro - ifeq ($(strip $(NO_NACP)),) $(OUTPUT).nro : $(OUTPUT).elf $(OUTPUT).nacp $(ROMFS_DEPS) else diff --git a/include/fs.h b/include/fs.h index a50fc5c..b4eb7ab 100644 --- a/include/fs.h +++ b/include/fs.h @@ -29,8 +29,11 @@ namespace fs { +/// \brief Print size in human-readable format (KiB, MiB, etc) +/// \param size_ Size to print std::string printSize (std::uint64_t size_); +/// \brief File I/O object class File { public: @@ -40,51 +43,97 @@ public: File (File const &that_) = delete; + /// \brief Move constructor + /// \param that_ Object to move from File (File &&that_); File &operator= (File const &that_) = delete; + /// \brief Move assignment + /// \param that_ Object to move from File &operator= (File &&that_); - operator bool () const; - operator FILE * () const; + /// \brief bool cast operator + explicit operator bool () const; + /// \brief std::FILE* cast operator + operator std::FILE * () const; + + /// \brief Set buffer size + /// \param size_ Buffer size void setBufferSize (std::size_t size_); + /// \brief Open file + /// \param path_ Path to open + /// \param mode_ Access mode (\sa std::fopen) bool open (char const *path_, char const *mode_ = "rb"); + + /// \brief Close file void close (); + /// \brief Seek to file position + /// \param pos_ File position + /// \param origin_ Reference position (\sa std::fseek) ssize_t seek (std::size_t pos_, int origin_); + /// \brief Read data + /// \param data_ Output buffer + /// \param size_ Size to read + /// \note Can return partial reads ssize_t read (void *data_, std::size_t size_); + + /// \brief Read data + /// \param data_ Output buffer + /// \param size_ Size to read + /// \note Fails on partial reads and errors bool readAll (void *data_, std::size_t size_); + /// \brief Write data + /// \param data_ Input data + /// \param size_ Size to write + /// \note Can return partial writes ssize_t write (void const *data_, std::size_t size_); + + /// \brief Write data + /// \param data_ Input data + /// \param size_ Size to write + /// \note Fails on partials writes and errors bool writeAll (void const *data_, std::size_t size_); + /// \brief Read data + /// \tparam T Type to read template T read () { T data; - if (read (&data, sizeof (data)) != sizeof (data)) + if (!readAll (&data, sizeof (data))) std::abort (); return data; } + /// \brief Write data + /// \tparam T type to write + /// \param data_ Data to write template void write (T const &data_) { - if (write (&data_, sizeof (data_)) != sizeof (data_)) + if (!writeAll (&data_, sizeof (data_))) std::abort (); } private: + /// \brief Underlying std::FILE* std::unique_ptr m_fp{nullptr, nullptr}; + + /// \brief Buffer std::unique_ptr m_buffer; + + /// \brief Buffer size std::size_t m_bufferSize = 0; }; +/// Directory object class Dir { public: @@ -94,20 +143,35 @@ public: Dir (Dir const &that_) = delete; + /// \brief Move constructor + /// \param that_ Object to move from Dir (Dir &&that_); Dir &operator= (Dir const &that_) = delete; + /// \brief Move assignment + /// \param that_ Object to move from Dir &operator= (Dir &&that_); - operator bool () const; + /// \brief bool cast operator + explicit operator bool () const; + + /// \brief DIR* cast operator operator DIR * () const; + /// \brief Open directory + /// \param path_ Path to open bool open (char const *const path_); + + /// \brief Close directory void close (); + + /// \brief Read a directory entry + /// \note Returns nullptr on end-of-directory or error; check errno struct dirent *read (); private: + /// \brief Underlying DIR* std::unique_ptr m_dp{nullptr, nullptr}; }; } diff --git a/include/ftpServer.h b/include/ftpServer.h index cabe8a0..2174006 100644 --- a/include/ftpServer.h +++ b/include/ftpServer.h @@ -34,40 +34,63 @@ class FtpServer; using UniqueFtpServer = std::unique_ptr; +/// \brief FTP server class FtpServer { public: ~FtpServer (); + /// \brief Draw server and all of its sessions void draw (); + /// \brief Create server + /// \param port_ Port to listen on static UniqueFtpServer create (std::uint16_t port_); + /// \brief Update free space static void updateFreeSpace (); + /// \brief Server start time static std::time_t startTime (); private: + /// \brief Paramterized constructor + /// \param port_ Port to listen on FtpServer (std::uint16_t port_); + /// \brief Handle when start button is pressed void handleStartButton (); + + /// \brief Handle when stop button is pressed void handleStopButton (); + /// \brief Server loop void loop (); + + /// \brief Thread entry point void threadFunc (); + /// \brief Thread platform::Thread m_thread; + + /// \brief Mutex platform::Mutex m_lock; + /// \brief Listen socket UniqueSocket m_socket; + /// \brief ImGui window name std::string m_name; + /// \brief Log SharedLog m_log; + /// \brief Sessions std::vector m_sessions; - std::uint16_t m_port; + /// \brief Port to listen on + std::uint16_t const m_port; + /// \brief Whether thread should quit std::atomic m_quit; }; diff --git a/include/ftpSession.h b/include/ftpSession.h index fbcdbbb..d7665d3 100644 --- a/include/ftpSession.h +++ b/include/ftpSession.h @@ -34,33 +34,54 @@ class FtpSession; using UniqueFtpSession = std::unique_ptr; +/// \brief FTP session class FtpSession { public: ~FtpSession (); + /// \brief Whether session sockets are all inactive bool dead (); + /// \brief Draw session status void draw (); + /// \brief Create session + /// \param commandSocket_ Command socket static UniqueFtpSession create (UniqueSocket commandSocket_); + /// \brief Poll for activity + /// \param sessions_ Sessions to poll static void poll (std::vector const &sessions_); private: - constexpr static auto COMMAND_BUFFERSIZE = 4096; + /// \brief Command buffer size + constexpr static auto COMMAND_BUFFERSIZE = 4096; + + /// \brief Response buffer size constexpr static auto RESPONSE_BUFFERSIZE = 32768; - constexpr static auto XFER_BUFFERSIZE = 65536; - constexpr static auto FILE_BUFFERSIZE = 4 * XFER_BUFFERSIZE; + + /// \brief Transfer buffersize + constexpr static auto XFER_BUFFERSIZE = 65536; + + /// \brief File buffersize + constexpr static auto FILE_BUFFERSIZE = 4 * XFER_BUFFERSIZE; #ifdef _3DS - constexpr static auto SOCK_BUFFERSIZE = 32768; + /// \brief Socket buffer size + constexpr static auto SOCK_BUFFERSIZE = 32768; + + /// \brief Amount of file position history to keep constexpr static auto POSITION_HISTORY = 100; #else - constexpr static auto SOCK_BUFFERSIZE = XFER_BUFFERSIZE; + /// \brief Socket buffer size + constexpr static auto SOCK_BUFFERSIZE = XFER_BUFFERSIZE; + + /// \brief Amount of file position history to keep constexpr static auto POSITION_HISTORY = 300; #endif + /// \brief Session state enum class State { COMMAND, @@ -68,6 +89,7 @@ private: DATA_TRANSFER, }; + /// \brief Transfer file mode enum class XferFileMode { RETR, @@ -75,6 +97,7 @@ private: APPE, }; + /// \brief Transfer directory mode enum class XferDirMode { LIST, @@ -84,122 +107,322 @@ private: STAT, }; + /// \brief Parameterized constructor + /// \param commandSocket_ Command socket FtpSession (UniqueSocket commandSocket_); + /// \brief Set session state + /// \param state_ State to set + /// \param closePasv_ Whether to close listening socket + /// \param closeData_ Whether to close data socket void setState (State state_, bool closePasv_, bool closeData_); + /// \brief Close data socket void closeData (); + /// \brief Change working directory bool changeDir (char const *args_); + /// \brief Accept connection as data socket bool dataAccept (); + + /// \brief Connect data socket bool dataConnect (); - void updateFreeSpace (); - + /// \brief Fill directory entry + /// \param st_ Entry status + /// \param path_ Path name + /// \param type_ MLST type int fillDirent (struct stat const &st_, std::string_view path_, char const *type_ = nullptr); + + /// \brief Fill directory entry + /// \param path_ Path name + /// \param type_ MLST type int fillDirent (std::string const &path_, char const *type_ = nullptr); + + /// \brief Transfer file + /// \param args_ Command arguments + /// \param mode_ Transfer file mode void xferFile (char const *args_, XferFileMode mode_); + + /// \brief Transfer directory + /// \param args_ Command arguments + /// \param mode_ Transfer directory mode void xferDir (char const *args_, XferDirMode mode_, bool workaround_); + /// \brief Read command + /// \param events_ Poll events void readCommand (int events_); + + /// \brief Write response void writeResponse (); + /// \brief Send response + /// \param fmt_ Message format __attribute__ ((format (printf, 2, 3))) void sendResponse (char const *fmt_, ...); + + /// \brief Send response + /// \param response_ Response message void sendResponse (std::string_view response_); + /// \brief Transfer function bool (FtpSession::*m_transfer) () = nullptr; + /// \brief Transfer directory list bool listTransfer (); + + /// \brief Transfer download bool retrieveTransfer (); + + /// \brief Transfer upload bool storeTransfer (); + /// \brief Mutex platform::Mutex m_lock; + /// \brief Command socket SharedSocket m_commandSocket; + + /// \brief Data listen socker UniqueSocket m_pasvSocket; + + /// \brief Data socket SharedSocket m_dataSocket; + + /// \brief Sockets pending close std::vector m_pendingCloseSocket; + /// \brief Command buffer IOBuffer m_commandBuffer; + + /// \brief Response buffer IOBuffer m_responseBuffer; + + /// \brief Transfer buffer IOBuffer m_xferBuffer; - SockAddr m_pasvAddr; + /// \brief Address from last PORT command SockAddr m_portAddr; + /// \brief Current working directory std::string m_cwd = "/"; + + /// \brief List working directory std::string m_lwd; + + /// \brief Path from RNFR command std::string m_rename; + + /// \brief Current work item std::string m_workItem; + /// \brief ImGui window name std::string m_windowName; + + /// \brief ImGui plot widget name std::string m_plotName; + /// \brief Position from REST command std::uint64_t m_restartPosition = 0; - std::uint64_t m_filePosition = 0; - std::uint64_t m_fileSize = 0; + /// \brief Current file position + std::uint64_t m_filePosition = 0; + + /// \brief File size of current transfer + std::uint64_t m_fileSize = 0; + + /// \brief Last file position update timestamp platform::steady_clock::time_point m_filePositionTime; + + /// \brief File position history std::uint64_t m_filePositionHistory[POSITION_HISTORY]; + + /// \brief File position history deltas float m_filePositionDeltas[POSITION_HISTORY]; + + /// \brief Transfer rate (EWMA low-pass filtered) float m_xferRate; + /// \brief Session state State m_state = State::COMMAND; + /// \brief File being transferred fs::File m_file; + + /// \brief Directory being transferred fs::Dir m_dir; + /// \brief Directory transfer mode XferDirMode m_xferDirMode; + /// \brief Whether previous command was PASV bool m_pasv : 1; + /// \brief Whether previous command was PORT bool m_port : 1; + /// \brief Whether receiving data bool m_recv : 1; + /// \brief Whether sending data bool m_send : 1; + /// \brief Whether urgent (out-of-band) data is on the way bool m_urgent : 1; + /// \brief Whether MLST type fact is enabled bool m_mlstType : 1; + /// \brief Whether MLST size fact is enabled bool m_mlstSize : 1; + /// \brief Whether MLST modify fact is enabled bool m_mlstModify : 1; + /// \brief Whether MLST perm fact is enabled bool m_mlstPerm : 1; + /// \brief Whether MLST unix.mode fact is enabled bool m_mlstUnixMode : 1; + /// \brief Abort a transfer + /// \param args_ Command arguments void ABOR (char const *args_); + + /// \brief Allocate space + /// \param args_ Command arguments void ALLO (char const *args_); + + /// \brief Append data to a file + /// \param args_ Command arguments void APPE (char const *args_); + + /// \brief CWD to parent directory + /// \param args_ Command arguments void CDUP (char const *args_); + + /// \brief Change working directory + /// \param args_ Command arguments void CWD (char const *args_); + + /// \brief Delete a file + /// \param args_ Command arguments void DELE (char const *args_); + + /// \brief List server features + /// \param args_ Command arguments void FEAT (char const *args_); + + /// \brief Print server help + /// \param args_ Command arguments void HELP (char const *args_); + + /// \brief List directory + /// \param args_ Command arguments void LIST (char const *args_); + + /// \brief Last modification time + /// \param args_ Command arguments void MDTM (char const *args_); + + /// \brief Create a directory + /// \param args_ Command arguments void MKD (char const *args_); + + /// \brief Machine list directory + /// \param args_ Command arguments void MLSD (char const *args_); + + /// \brief Machine list + /// \param args_ Command arguments void MLST (char const *args_); + + /// \brief Set transfer mode + /// \param args_ Command arguments void MODE (char const *args_); + + /// \brief Name list + /// \param args_ Command arguments void NLST (char const *args_); + + /// \brief No-op + /// \param args_ Command arguments void NOOP (char const *args_); + + /// \brief Set server options + /// \param args_ Command arguments void OPTS (char const *args_); + + /// \brief Password + /// \param args_ Command arguments void PASS (char const *args_); + + /// \brief Request an address to connect to for data transfers + /// \param args_ Command arguments void PASV (char const *args_); + + /// \brief Provide an address to connect to for data transfers + /// \param args_ Command arguments void PORT (char const *args_); + + /// \brief Print working directory + /// \param args_ Command arguments void PWD (char const *args_); + + /// \brief Terminate session + /// \param args_ Command arguments void QUIT (char const *args_); + + /// \brief Restart a file transfer + /// \param args_ Command arguments void REST (char const *args_); + + /// \brief Retrieve a file + /// \param args_ Command arguments + /// \note Requires a PASV or PORT connection void RETR (char const *args_); + + /// \brief Remove a directory + /// \param args_ Command arguments void RMD (char const *args_); + + /// \brief Rename from + /// \param args_ Command arguments void RNFR (char const *args_); + + /// \brief Rename to + /// \param args_ Command arguments void RNTO (char const *args_); + + /// \brief Get file size + /// \param args_ Command arguments void SIZE (char const *args_); + + /// \brief Get status + /// \param args_ Command arguments + /// \note If no argument is supplied, and a transfer is occurring, get the current transfer + /// status. If no argument is supplied, and no transfer is occurring, get the server status. If + /// an argument is supplied, this is equivalent to LIST, except the data is sent over the + /// command socket. void STAT (char const *args_); + + /// \brief Store a file + /// \param args_ Command arguments void STOR (char const *args_); + + /// \brief Store a unique file + /// \param args_ Command arguments void STOU (char const *args_); + + /// \brief Set file structure + /// \param args_ Command arguments void STRU (char const *args_); + + /// \brief Identify system + /// \param args_ Command arguments void SYST (char const *args_); + + /// \brief Set representation type + /// \param args_ Command arguments void TYPE (char const *args_); + + /// \brief User name + /// \param args_ Command arguments void USER (char const *args_); + /// \brief Map of command handlers static std::vector> const handlers; }; diff --git a/include/ioBuffer.h b/include/ioBuffer.h index fd8c961..5633442 100644 --- a/include/ioBuffer.h +++ b/include/ioBuffer.h @@ -23,29 +23,66 @@ #include #include +/// \brief I/O buffer +/// [unusable][usedArea][freeArea] class IOBuffer { public: ~IOBuffer (); + /// \brief Parameterized constructor + /// \param size_ Buffer size IOBuffer (std::size_t size_); + /// \brief Get pointer to writable area char *freeArea () const; + /// \brief Get size of writable area std::size_t freeSize () const; - void markFree (std::size_t size_); + /// \brief Get pointer to readable area char *usedArea () const; + /// \brief Get size of readable area std::size_t usedSize () const; + + /// \brief Consume data from the beginning of usedArea + /// \param size_ Size to consume + /// [unusable][+++++usedArea][freeArea] + /// becomes + /// [unusable+++++][usedArea][freeArea] + void markFree (std::size_t size_); + /// \brief Produce data to the end of usedArea from freeArea + /// [unusable][usedArea][++++++freeArea] + /// becomes + /// [unusable][usedArea++++++][freeArea] void markUsed (std::size_t size_); + /// \brief Whether usedArea is empty bool empty () const; + + /// \brief Get buffer capacity std::size_t capacity () const; + + /// \brief Clear buffer; usedArea becomes empty + /// [unusable][usedArea][++++++freeArea] + /// becomes + /// [freeArea++++++++++++++++++++++++++] void clear (); + + /// \brief Move usedArea to the beginning of the buffer + /// [unusable][usedArea][freeArea] + /// becomes + /// [usedArea][freeArea++++++++++] void coalesce (); private: + /// \brief Buffer std::unique_ptr m_buffer; + + /// \brief Buffer size std::size_t const m_size; + + /// \brief Start of usedArea std::size_t m_start = 0; - std::size_t m_end = 0; + /// \brief Start of freeArea + std::size_t m_end = 0; }; diff --git a/include/log.h b/include/log.h index 1afcb05..b8e765b 100644 --- a/include/log.h +++ b/include/log.h @@ -32,9 +32,11 @@ class Log; using SharedLog = std::shared_ptr; using WeakLog = std::weak_ptr; +/// \brief Log object class Log { public: + /// \brief Log level enum Level { DEBUG, @@ -46,36 +48,72 @@ public: ~Log (); + /// \brief Draw log void draw (); + /// \brief Create log static SharedLog create (); + + /// \brief Bind log + /// \param log_ Log to bind static void bind (SharedLog log_); + /// \brief Add debug message to bound log + /// \param fmt_ Message format __attribute__ ((format (printf, 1, 2))) static void debug (char const *fmt_, ...); + /// \brief Add info message to bound log + /// \param fmt_ Message format __attribute__ ((format (printf, 1, 2))) static void info (char const *fmt_, ...); + /// \brief Add error message to bound log + /// \param fmt_ Message format __attribute__ ((format (printf, 1, 2))) static void error (char const *fmt_, ...); + /// \brief Add command message to bound log + /// \param fmt_ Message format __attribute__ ((format (printf, 1, 2))) static void command (char const *fmt_, ...); + /// \brief Add response message to bound log + /// \param fmt_ Message format __attribute__ ((format (printf, 1, 2))) static void response (char const *fmt_, ...); + /// \brief Add log message to bound log + /// \param level_ Log level + /// \param fmt_ Message format + /// \param ap_ Message arguments static void log (Level level_, char const *fmt_, va_list ap_); + + /// \brief Add log message to bound log + /// \param level_ Log level + /// \param message_ Message to log static void log (Level level_, std::string_view message_); private: Log (); + /// \brief Add log message + /// \param level_ Log level + /// \param fmt_ Message format + /// \param ap_ Message arguments void _log (Level level_, char const *fmt_, va_list ap_); + /// \brief Log message struct Message { + /// \brief Parameterized constructor + /// \param level_ Log level + /// \param message_ Log message Message (Level const level_, std::string message_) : level (level_), message (std::move (message_)) { } + /// \brief Log level Level level; + /// \brief Log message std::string message; }; + /// \brief Log messages std::vector m_messages; + + /// \brief Log lock platform::Mutex m_lock; }; diff --git a/include/platform.h b/include/platform.h index 689d3b5..30e6cd5 100644 --- a/include/platform.h +++ b/include/platform.h @@ -31,65 +31,102 @@ namespace platform { +/// \brief Initialize platform bool init (); + +/// \brief Platform loop bool loop (); + +/// \brief Platform render void render (); + +/// \brief Deinitialize platform void exit (); #ifdef _3DS +/// \brief Steady clock struct steady_clock { - using rep = std::uint64_t; - using period = std::ratio<1, SYSCLOCK_ARM11>; - using duration = std::chrono::duration; + /// \brief Type representing number of ticks + using rep = std::uint64_t; + + /// \brief Type representing ratio of clock period in seconds + using period = std::ratio<1, SYSCLOCK_ARM11>; + + /// \brief Duration type + using duration = std::chrono::duration; + + /// \brief Timestamp type using time_point = std::chrono::time_point; + /// \brief Whether clock is steady constexpr static bool is_steady = true; + + /// \brief Current timestamp static time_point now () noexcept { return time_point (duration (svcGetSystemTick ())); } }; #else +/// \brief Steady clock using steady_clock = std::chrono::steady_clock; #endif +/// \brief Platform thread class Thread { public: ~Thread (); Thread (); + /// \brief Parameterized constructor + /// \param func_ Thread entrypoint Thread (std::function func_); Thread (Thread const &that_) = delete; + /// \brief Move constructor + /// \param that_ Object to move from Thread (Thread &&that_); Thread &operator= (Thread const &that_) = delete; + /// \brief Move assignment + /// \param that_ Object to move from Thread &operator= (Thread &&that_); + /// \brief Join thread void join (); + /// \brief Suspend current thread + /// \param timeout_ Minimum time to sleep static void sleep (std::chrono::milliseconds timeout_); private: class privateData_t; + + /// \brief pimpl std::unique_ptr m_d; }; +/// \brief Platform mutex class Mutex { public: ~Mutex (); Mutex (); + /// \brief Lock mutex void lock (); + + /// \brief Unlock mutex void unlock (); private: class privateData_t; + + /// \brief pimpl std::unique_ptr m_d; }; } diff --git a/include/sockAddr.h b/include/sockAddr.h index d44f30e..aed0b43 100644 --- a/include/sockAddr.h +++ b/include/sockAddr.h @@ -25,6 +25,7 @@ #include +/// \brief Socket address class SockAddr { public: @@ -32,39 +33,72 @@ public: SockAddr (); + /// \brief Copy constructor + /// \param that_ Object to copy SockAddr (SockAddr const &that_); + /// \brief Move constructor + /// \param that_ Object to move from SockAddr (SockAddr &&that_); + /// \brief Copy assignment + /// \param that_ Object to copy SockAddr &operator= (SockAddr const &that_); + /// \brief Move assignment + /// \param that_ Object to move from SockAddr &operator= (SockAddr &&that_); + /// \param Parameterized constructor + /// \param addr_ Address SockAddr (struct sockaddr const &addr_); + /// \param Parameterized constructor + /// \param addr_ Address SockAddr (struct sockaddr_in const &addr_); #ifndef _3DS + /// \param Parameterized constructor + /// \param addr_ Address SockAddr (struct sockaddr_in6 const &addr_); #endif + /// \param Parameterized constructor + /// \param addr_ Address SockAddr (struct sockaddr_storage const &addr_); + /// \param sockaddr_in cast operator operator struct sockaddr_in const & () const; #ifndef _3DS + /// \param sockaddr_in6 cast operator operator struct sockaddr_in6 const & () const; #endif + /// \param sockaddr_storage cast operator operator struct sockaddr_storage const & () const; + /// \param sockaddr* cast operator operator struct sockaddr * (); + /// \param sockaddr const* cast operator operator struct sockaddr const * () const; + /// \brief Address port std::uint16_t port () const; + + /// \brief Address name + /// \param buffer_ + /// \param size_ Size of buffer_ + /// \retval buffer_ success + /// \retval nullptr failure char const *name (char *buffer_, std::size_t size_) const; + + /// \brief Address name + /// \retval nullptr failure + /// \note This function is not reentrant char const *name () const; private: + /// \brief Address storage struct sockaddr_storage m_addr = {}; }; diff --git a/include/socket.h b/include/socket.h index c3fac2a..12b2849 100644 --- a/include/socket.h +++ b/include/socket.h @@ -30,53 +30,115 @@ class Socket; using UniqueSocket = std::unique_ptr; using SharedSocket = std::shared_ptr; +/// \brief Socket object class Socket { public: + /// \brief Poll info struct PollInfo { + /// \brief Socket to poll std::reference_wrapper socket; + + /// \brief Input events int events; + + /// \brief Output events int revents; }; ~Socket (); + /// \brief Accept connection UniqueSocket accept (); + + /// \brief Whether socket is at out-of-band mark int atMark (); + + /// \brief Bind socket to address + /// \param addr_ Address to bind bool bind (SockAddr const &addr_); + + /// \brief Connect to a peer + /// \param addr_ Peer address bool connect (SockAddr const &addr_); + + /// \brief Listen for connections + /// \param backlog_ Queue size for incoming connections bool listen (int backlog_); + + /// \brief Shutdown socket + /// \param how_ Type of shutdown (\sa ::shutdown) bool shutdown (int how_); + /// \brief Set linger option + /// \param enable_ Whether to enable linger + /// \param time_ Linger timeout bool setLinger (bool enable_, std::chrono::seconds time_); + + /// \brief Set non-blocking + /// \param nonBlocking_ Whether to set non-blocking bool setNonBlocking (bool nonBlocking_ = true); + + /// \brief Set reuse address in subsequent bind + /// \param reuse_ Whether to reuse address bool setReuseAddress (bool reuse_ = true); + + /// \brief Set recv buffer size + /// \param size_ Buffer size bool setRecvBufferSize (std::size_t size_); + + /// \brief Set send buffer size + /// \param size_ Buffer size bool setSendBufferSize (std::size_t size_); + /// \brief Read data + /// \param buffer_ Output buffer + /// \param size_ Size to read + /// \param oob_ Whether to read from out-of-band ssize_t read (void *buffer_, std::size_t size_, bool oob_ = false); + + /// \brief Read data + /// \param buffer_ Output buffer + /// \param size_ Size to read + /// \param oob_ Whether to read from out-of-band ssize_t read (IOBuffer &buffer_, bool oob_ = false); + + /// \brief Write data + /// \param buffer_ Input buffer + /// \param size_ Size to write ssize_t write (void const *buffer_, std::size_t size_); + + /// \brief Write data + /// \param buffer_ Input buffer + /// \param size_ Size to write ssize_t write (IOBuffer &buffer_); + /// \brief Local name SockAddr const &sockName () const; + /// \brief Peer name SockAddr const &peerName () const; + /// \brief Create socket static UniqueSocket create (); + /// \brief Poll sockets + /// \param info_ Poll info + /// \param count_ Number of poll entries + /// \param timeout_ Poll timeout static int poll (PollInfo *info_, std::size_t count_, std::chrono::milliseconds timeout_); - int fd () const - { - return m_fd; - } - private: Socket () = delete; + /// \brief Parameterized constructor + /// \param fd_ Socket fd Socket (int fd_); + /// \brief Parameterized constructor + /// \param fd_ Socket fd + /// \param sockName_ Local name + /// \param peerName_ Peer name Socket (int fd_, SockAddr const &sockName_, SockAddr const &peerName_); Socket (Socket const &that_) = delete; @@ -87,11 +149,17 @@ private: Socket &operator= (Socket &&that_) = delete; + /// \param Local name SockAddr m_sockName; + /// \param Peer name SockAddr m_peerName; + /// \param Socket fd int const m_fd; + /// \param Whether listening bool m_listening : 1; + + /// \param Whether connected bool m_connected : 1; }; diff --git a/source/3ds/imgui_citro3d.cpp b/source/3ds/imgui_citro3d.cpp index 48e7c74..7c5d005 100644 --- a/source/3ds/imgui_citro3d.cpp +++ b/source/3ds/imgui_citro3d.cpp @@ -33,37 +33,59 @@ namespace { +/// \brief 3DS font glyph ranges std::vector s_fontRanges; +/// \brief Clear color constexpr auto CLEAR_COLOR = 0x204B7AFF; +/// \brief Display transfer flags constexpr auto DISPLAY_TRANSFER_FLAGS = GX_TRANSFER_FLIP_VERT (0) | GX_TRANSFER_OUT_TILED (0) | GX_TRANSFER_RAW_COPY (0) | GX_TRANSFER_IN_FORMAT (GX_TRANSFER_FMT_RGBA8) | GX_TRANSFER_OUT_FORMAT (GX_TRANSFER_FMT_RGB8) | GX_TRANSFER_SCALING (GX_TRANSFER_SCALE_NO); -C3D_RenderTarget *s_top = nullptr; +/// \brief Top screen render target +C3D_RenderTarget *s_top = nullptr; +/// \brief Bottom screen render target C3D_RenderTarget *s_bottom = nullptr; +/// \brief Vertex shader DVLB_s *s_vsh = nullptr; +/// \brief Vertex shader program shaderProgram_s s_program; +/// \brief Projection matrix uniform location int s_projLocation; +/// \brief Top screen projection matrix C3D_Mtx s_projTop; +/// \brief Bottom screen projection matrix C3D_Mtx s_projBottom; +/// \brief System font textures std::vector s_fontTextures; +/// \brief Text scale float s_textScale; +/// \brief Scissor test bounds std::uint32_t s_boundScissor[4]; +/// \brief Currently bound vertex data ImDrawVert *s_boundVtxData; +/// \brief Currently bound texture C3D_Tex *s_boundTexture; +/// \brief Vertex data buffer ImDrawVert *s_vtxData = nullptr; +/// \brief Size of vertex data buffer std::size_t s_vtxSize = 0; -ImDrawIdx *s_idxData = nullptr; +/// \brief Index data buffer +ImDrawIdx *s_idxData = nullptr; +/// \brief Size of index data buffer std::size_t s_idxSize = 0; +/// \brief Get code point from glyph index +/// \param font_ Font to search +/// \param glyphIndex_ Glyph index std::uint32_t fontCodePointFromGlyphIndex (CFNT_s *const font_, int const glyphIndex_) { for (auto cmap = fontGetInfo (font_)->cmap; cmap; cmap = cmap->next) @@ -100,8 +122,11 @@ std::uint32_t fontCodePointFromGlyphIndex (CFNT_s *const font_, int const glyphI return 0; } +/// \brief Setup render state +/// \param screen_ Whether top or bottom screen void setupRenderState (gfxScreen_t const screen_) { + // disable face culling C3D_CullFace (GPU_CULL_NONE); // configure attributes for user with vertex shader @@ -111,14 +136,18 @@ void setupRenderState (gfxScreen_t const screen_) AttrInfo_AddLoader (attrInfo, 1, GPU_FLOAT, 2); // v1 = inUv AttrInfo_AddLoader (attrInfo, 2, GPU_UNSIGNED_BYTE, 4); // v2 = inColor + // clear bindings std::memset (s_boundScissor, 0xFF, sizeof (s_boundScissor)); s_boundVtxData = nullptr; s_boundTexture = nullptr; + // bind program C3D_BindProgram (&s_program); + // enable depth test C3D_DepthTest (true, GPU_GREATER, GPU_WRITE_COLOR); + // enable alpha blending C3D_AlphaBlend (GPU_BLEND_ADD, GPU_BLEND_ADD, GPU_SRC_ALPHA, @@ -126,6 +155,7 @@ void setupRenderState (gfxScreen_t const screen_) GPU_SRC_ALPHA, GPU_ONE_MINUS_SRC_ALPHA); + // apply projection matrix if (screen_ == GFX_TOP) C3D_FVUnifMtx4x4 (GPU_VERTEX_SHADER, s_projLocation, &s_projTop); else @@ -135,36 +165,46 @@ void setupRenderState (gfxScreen_t const screen_) void imgui::citro3d::init () { - // Setup back-end capabilities flags + // setup back-end capabilities flags ImGuiIO &io = ImGui::GetIO (); io.BackendRendererName = "citro3d"; io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; + // initialize citro3d C3D_Init (C3D_DEFAULT_CMDBUF_SIZE); + // create top screen render target s_top = C3D_RenderTargetCreate (240, 400, GPU_RB_RGBA8, GPU_RB_DEPTH24_STENCIL8); C3D_RenderTargetSetOutput (s_top, GFX_TOP, GFX_LEFT, DISPLAY_TRANSFER_FLAGS); + // create bottom screen render target s_bottom = C3D_RenderTargetCreate (240, 320, GPU_RB_RGBA8, GPU_RB_DEPTH24_STENCIL8); C3D_RenderTargetSetOutput (s_bottom, GFX_BOTTOM, GFX_LEFT, DISPLAY_TRANSFER_FLAGS); + // load vertex shader s_vsh = DVLB_ParseFile ( const_cast (reinterpret_cast (vshader_shbin)), vshader_shbin_size); + + // initialize vertex shader program shaderProgramInit (&s_program); shaderProgramSetVsh (&s_program, &s_vsh->DVLE[0]); + // get projection matrix uniform location s_projLocation = shaderInstanceGetUniformLocation (s_program.vertexShader, "proj"); + // initialize projection matrices Mtx_OrthoTilt (&s_projTop, 0.0f, 800.0f, 480.0f, 0.0f, -1.0f, 1.0f, false); Mtx_OrthoTilt (&s_projBottom, 80.0f, 720.0f, 960.0f, 480.0f, -1.0f, 1.0f, false); + // allocate vertex data buffer s_vtxSize = 65536; s_vtxData = reinterpret_cast (linearAlloc (sizeof (ImDrawVert) * s_vtxSize)); if (!s_vtxData) svcBreak (USERBREAK_PANIC); + // allocate index data buffer s_idxSize = 65536; s_idxData = reinterpret_cast (linearAlloc (sizeof (ImDrawIdx) * s_idxSize)); if (!s_idxData) @@ -184,6 +224,7 @@ void imgui::citro3d::init () s_textScale = 30.0f / glyphInfo->cellHeight; + // use system font sheets as citro3d textures for (unsigned i = 0; i < glyphInfo->nSheets; ++i) { auto &tex = s_fontTextures[i]; @@ -201,9 +242,11 @@ void imgui::citro3d::init () } { + // create texture for ImGui's white pixel auto &tex = s_fontTextures[glyphInfo->nSheets]; C3D_TexInit (&tex, 8, 8, GPU_A4); + // fill texture with full alpha std::uint32_t size; auto data = C3D_Tex2DGetImagePtr (&tex, 0, &size); if (!data || !size) @@ -211,10 +254,12 @@ void imgui::citro3d::init () std::memset (data, 0xFF, size); } + // get alternate character glyph ImWchar alterChar = fontCodePointFromGlyphIndex (font, fontInfo->alterCharIndex); if (!alterChar) alterChar = '?'; + // collect character map std::vector charSet; for (auto cmap = fontInfo->cmap; cmap; cmap = cmap->next) { @@ -242,9 +287,11 @@ void imgui::citro3d::init () if (charSet.empty ()) svcBreak (USERBREAK_PANIC); + // deduplicate character map std::sort (std::begin (charSet), std::end (charSet)); charSet.erase (std::unique (std::begin (charSet), std::end (charSet)), std::end (charSet)); + // fill in font glyph ranges auto it = std::begin (charSet); ImWchar start = *it++; ImWchar prev = start; @@ -262,8 +309,11 @@ void imgui::citro3d::init () } s_fontRanges.emplace_back (start); s_fontRanges.emplace_back (prev); + + // terminate glyph ranges s_fontRanges.emplace_back (0); + // initialize font atlas auto const atlas = ImGui::GetIO ().Fonts; atlas->Clear (); atlas->TexWidth = glyphInfo->sheetWidth; @@ -272,6 +322,7 @@ void imgui::citro3d::init () atlas->TexUvWhitePixel = ImVec2 (0.5f / 8.0f, glyphInfo->nSheets + 0.5f / 8.0f); atlas->TexPixelsAlpha8 = static_cast (IM_ALLOC (1)); // dummy allocation + // initialize font config ImFontConfig config; config.FontData = nullptr; config.FontDataSize = 0; @@ -292,19 +343,20 @@ void imgui::citro3d::init () config.EllipsisChar = 0x2026; std::memset (config.Name, 0, sizeof (config.Name)); + // create font auto const imFont = IM_NEW (ImFont); config.DstFont = imFont; + // add config and font to atlas atlas->ConfigData.push_back (config); atlas->Fonts.push_back (imFont); - // atlas->CustomRectIds[0] = atlas->AddCustomRectRegular (0x80000000, 108 * 2 + 1, 27); - // atlas->CustomRects[0].X = 0; - // atlas->CustomRects[0].Y = 0; atlas->SetTexID (s_fontTextures.data ()); + // initialize font metrics imFont->FallbackAdvanceX = fontInfo->defaultWidth.charWidth; imFont->FontSize = fontInfo->lineFeed; + // add glyphs to font fontGlyphPos_s glyphPos; for (auto const &code : charSet) { @@ -312,6 +364,7 @@ void imgui::citro3d::init () if (glyphIndex < 0) svcBreak (USERBREAK_PANIC); + // calculate glyph metrics fontCalcGlyphPos (&glyphPos, font, glyphIndex, @@ -319,8 +372,8 @@ void imgui::citro3d::init () 1.0f, 1.0f); + // convert to ImGui font metrics ImFontGlyph glyph; - glyph.Codepoint = code; glyph.AdvanceX = glyphPos.xAdvance; glyph.X0 = glyphPos.vtxcoord.left; @@ -332,41 +385,48 @@ void imgui::citro3d::init () glyph.U1 = glyphPos.texcoord.right; glyph.V1 = glyphPos.sheetIndex + glyphPos.texcoord.bottom; + // add glyph to font imFont->Glyphs.push_back (glyph); imFont->MetricsTotalSurface += static_cast ((glyph.U1 - glyph.U0) * atlas->TexWidth + 1.99f) * static_cast ((glyph.V1 - glyph.V0) * atlas->TexHeight + 1.99f); } + // build lookup table imFont->BuildLookupTable (); + // finalize font imFont->DisplayOffset.x = 0.0f; imFont->DisplayOffset.y = fontInfo->ascent; - imFont->ContainerAtlas = atlas; imFont->ConfigData = &atlas->ConfigData[0]; imFont->ConfigDataCount = 1; imFont->FallbackChar = alterChar; imFont->EllipsisChar = config.EllipsisChar; - imFont->Scale = 1.0f; + imFont->Scale = s_textScale; imFont->Ascent = fontInfo->ascent; imFont->Descent = 0.0f; } void imgui::citro3d::exit () { + // free vertex/index data buffers linearFree (s_idxData); linearFree (s_vtxData); + // delete ImGui white pixel texture assert (!s_fontTextures.empty ()); C3D_TexDelete (&s_fontTextures.back ()); + // free shader program shaderProgramFree (&s_program); DVLB_Free (s_vsh); + // free render targets C3D_RenderTargetDelete (s_bottom); C3D_RenderTargetDelete (s_top); + // deinitialize citro3d C3D_Fini (); } @@ -377,9 +437,12 @@ void imgui::citro3d::newFrame () void imgui::citro3d::render () { C3D_FrameBegin (C3D_FRAME_SYNCDRAW); + + // clear frame/depth buffers C3D_RenderTargetClear (s_top, C3D_CLEAR_ALL, CLEAR_COLOR, 0); C3D_RenderTargetClear (s_bottom, C3D_CLEAR_ALL, CLEAR_COLOR, 0); + // get draw data auto const drawData = ImGui::GetDrawData (); if (drawData->CmdListsCount <= 0) { @@ -387,6 +450,7 @@ void imgui::citro3d::render () 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) @@ -395,6 +459,7 @@ void imgui::citro3d::render () return; } + // check if we need to grow vertex data buffer if (s_vtxSize < static_cast (drawData->TotalVtxCount)) { linearFree (s_vtxData); @@ -406,6 +471,7 @@ void imgui::citro3d::render () svcBreak (USERBREAK_PANIC); } + // check if we need to grow index data buffer if (s_idxSize < static_cast (drawData->TotalIdxCount)) { // add 10% to avoid growing many frames in a row @@ -415,7 +481,7 @@ void imgui::citro3d::render () svcBreak (USERBREAK_PANIC); } - // Will project scissor/clipping rectangles into framebuffer space + // 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) @@ -427,11 +493,14 @@ void imgui::citro3d::render () for (int i = 0; i < drawData->CmdListsCount; ++i) { auto const &cmdList = *drawData->CmdLists[i]; + + // double check that we don't overrun vertex/index data buffers if (s_vtxSize - offsetVtx < static_cast (cmdList.VtxBuffer.Size)) svcBreak (USERBREAK_PANIC); if (s_idxSize - offsetIdx < static_cast (cmdList.IdxBuffer.Size)) svcBreak (USERBREAK_PANIC); + // copy vertex/index data into buffers std::memcpy (&s_vtxData[offsetVtx], cmdList.VtxBuffer.Data, sizeof (ImDrawVert) * cmdList.VtxBuffer.Size); @@ -455,7 +524,7 @@ void imgui::citro3d::render () offsetVtx = 0; offsetIdx = 0; - // Render command lists + // render command lists for (int i = 0; i < drawData->CmdListsCount; ++i) { auto const &cmdList = *drawData->CmdLists[i]; @@ -463,7 +532,7 @@ void imgui::citro3d::render () { if (cmd.UserCallback) { - // User callback, registered via ImDrawList::AddCallback() + // 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) @@ -473,7 +542,7 @@ void imgui::citro3d::render () } else { - // Project scissor/clipping rectangles into framebuffer space + // 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; @@ -486,6 +555,10 @@ void imgui::citro3d::render () 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; if (screen == GFX_TOP) { @@ -493,12 +566,22 @@ void imgui::citro3d::render () if (clip.y > 240.0f) continue; + // convert from framebuffer space to screen space (3DS screen rotation) auto const x1 = std::clamp (240.0f - clip.w, 0, 240); auto const y1 = std::clamp (400.0f - clip.z, 0, 400); auto const x2 = std::clamp (240.0f - clip.y, 0, 240); auto const y2 = std::clamp (400.0f - clip.x, 0, 400); - C3D_SetScissor (GPU_SCISSOR_NORMAL, x1, y1, x2, y2); + // check if scissor needs to be updated + if (s_boundScissor[0] != x1 || s_boundScissor[1] != y1 || + s_boundScissor[2] != x2 || s_boundScissor[3] != y2) + { + s_boundScissor[0] = x1; + s_boundScissor[1] = y1; + s_boundScissor[2] = x2; + s_boundScissor[3] = y2; + C3D_SetScissor (GPU_SCISSOR_NORMAL, x1, y1, x2, y2); + } } else { @@ -514,11 +597,14 @@ void imgui::citro3d::render () if (clip.x > 360.0f) continue; + // convert from framebuffer space to screen space + // (3DS screen rotation + bottom screen offset) auto const x1 = std::clamp (480.0f - clip.w, 0, 240); auto const y1 = std::clamp (360.0f - clip.z, 0, 320); auto const x2 = std::clamp (480.0f - clip.y, 0, 240); auto const y2 = std::clamp (360.0f - clip.x, 0, 320); + // check if scissor needs to be updated if (s_boundScissor[0] != x1 || s_boundScissor[1] != y1 || s_boundScissor[2] != x2 || s_boundScissor[3] != y2) { @@ -530,36 +616,41 @@ void imgui::citro3d::render () } } + // check if we need to update vertex data binding auto const vtxData = &s_vtxData[cmd.VtxOffset + offsetVtx]; if (vtxData != s_boundVtxData) { - s_boundVtxData = &s_vtxData[cmd.VtxOffset + offsetVtx]; + s_boundVtxData = vtxData; auto const bufInfo = C3D_GetBufInfo (); BufInfo_Init (bufInfo); - BufInfo_Add (bufInfo, s_boundVtxData, sizeof (ImDrawVert), 3, 0x210); + BufInfo_Add (bufInfo, vtxData, sizeof (ImDrawVert), 3, 0x210); } + // check if we need to update texture binding auto tex = static_cast (cmd.TextureId); if (tex == s_fontTextures.data ()) { assert (cmd.ElemCount % 3 == 0); - // TODO get by idx not consecutive vtx + // get sheet number from uv coords auto const getSheet = [] (auto const vtx_, auto const idx_) { unsigned const sheet = std::min ( {vtx_[idx_[0]].uv.y, vtx_[idx_[1]].uv.y, vtx_[idx_[2]].uv.y}); + + // assert that these three vertices use the same sheet for (unsigned i = 0; i < 3; ++i) assert (vtx_[idx_[i]].uv.y - sheet <= 1.0f); return sheet; }; + // initialize texture binding unsigned boundSheet = getSheet (&s_vtxData[cmd.VtxOffset + offsetVtx], &s_idxData[cmd.IdxOffset + offsetIdx]); + C3D_TexBind (0, &s_fontTextures[boundSheet]); unsigned offset = 0; - C3D_TexBind (0, &s_fontTextures[boundSheet]); - + // update texture environment for non-image drawing auto const env = C3D_GetTexEnv (0); C3D_TexEnvInit (env); C3D_TexEnvSrc ( @@ -569,23 +660,30 @@ void imgui::citro3d::render () env, C3D_Alpha, GPU_TEXTURE0, GPU_PRIMARY_COLOR, GPU_PRIMARY_COLOR); C3D_TexEnvFunc (env, C3D_Alpha, GPU_MODULATE); + // process one triangle at a time for (unsigned i = 3; i < cmd.ElemCount; i += 3) { + // get sheet for this triangle unsigned const sheet = getSheet (&s_vtxData[cmd.VtxOffset + offsetVtx], &s_idxData[cmd.IdxOffset + offsetIdx + i]); + + // check if we're changing textures if (boundSheet != sheet) { + // draw everything up until now C3D_DrawElements (GPU_TRIANGLES, i - offset, C3D_UNSIGNED_SHORT, &s_idxData[cmd.IdxOffset + offsetIdx + offset]); + // bind texture for next draw call boundSheet = sheet; offset = i; C3D_TexBind (0, &s_fontTextures[boundSheet]); } } + // draw the final set of triangles assert ((cmd.ElemCount - offset) % 3 == 0); C3D_DrawElements (GPU_TRIANGLES, cmd.ElemCount - offset, @@ -594,9 +692,13 @@ void imgui::citro3d::render () } else { + // drawing an image; check if we need to change texture binding if (tex != s_boundTexture) { + // bind new texture C3D_TexBind (0, tex); + + // update texture environment for drawing images auto const env = C3D_GetTexEnv (0); C3D_TexEnvInit (env); C3D_TexEnvSrc ( @@ -604,6 +706,7 @@ void imgui::citro3d::render () C3D_TexEnvFunc (env, C3D_Both, GPU_MODULATE); } + // draw triangles C3D_DrawElements (GPU_TRIANGLES, cmd.ElemCount, C3D_UNSIGNED_SHORT, diff --git a/source/3ds/imgui_citro3d.h b/source/3ds/imgui_citro3d.h index a8e53ab..da078e2 100644 --- a/source/3ds/imgui_citro3d.h +++ b/source/3ds/imgui_citro3d.h @@ -24,10 +24,14 @@ namespace imgui { namespace citro3d { +/// \brief Initialize citro3d void init (); +/// \brief Deinitialize citro3d void exit (); +/// \brief Prepare citro3d for a new frame void newFrame (); +/// \brief Render ImGui draw list void render (); } } diff --git a/source/3ds/imgui_ctru.cpp b/source/3ds/imgui_ctru.cpp index b04d314..7e6d6bf 100644 --- a/source/3ds/imgui_ctru.cpp +++ b/source/3ds/imgui_ctru.cpp @@ -23,6 +23,7 @@ #include "imgui.h" #include "fs.h" +#include "platform.h" #include <3ds.h> @@ -35,32 +36,45 @@ using namespace std::chrono_literals; namespace { -constexpr auto SCREEN_WIDTH = 400.0f; +/// \brief Screen width +constexpr auto SCREEN_WIDTH = 400.0f; +/// \brief Screen height constexpr auto SCREEN_HEIGHT = 480.0f; +/// \brief Clipboard std::string s_clipboard; +/// \brief Get clipboard text callback +/// \param userData_ User data char const *getClipboardText (void *const userData_) { (void)userData_; return s_clipboard.c_str (); } +/// \brief Set clipboard text callback +/// \param userData_ User data +/// \param text_ Clipboard text void setClipboardText (void *const userData_, char const *const text_) { (void)userData_; s_clipboard = text_; } +/// \brief Update touch position +/// \param io_ ImGui IO void updateTouch (ImGuiIO &io_) { - if (!((hidKeysDown () | hidKeysHeld ()) & KEY_TOUCH)) + // check if touchpad was touched + if (!(hidKeysHeld () & KEY_TOUCH)) { + // set mouse cursor off-screen io_.MousePos = ImVec2 (-10.0f, -10.0f); io_.MouseDown[0] = false; return; } + // read touch position touchPosition pos; hidTouchRead (&pos); @@ -69,8 +83,11 @@ void updateTouch (ImGuiIO &io_) io_.MouseDown[0] = true; } +/// \brief Update gamepad inputs +/// \param io_ ImGui IO void updateGamepads (ImGuiIO &io_) { + // clear navigation inputs std::memset (io_.NavInputs, 0, sizeof (io_.NavInputs)); auto const buttonMapping = { @@ -88,6 +105,7 @@ void updateGamepads (ImGuiIO &io_) std::make_pair (KEY_DLEFT, ImGuiNavInput_DpadLeft), }; + // read buttons from 3DS auto const keys = hidKeysHeld (); for (auto const &[in, out] : buttonMapping) { @@ -95,6 +113,7 @@ void updateGamepads (ImGuiIO &io_) io_.NavInputs[out] = 1.0f; } + // update joystick circlePosition cpad; auto const analogMapping = { std::make_tuple (std::ref (cpad.dx), ImGuiNavInput_LStickLeft, -0.3f, -0.9f), @@ -103,12 +122,12 @@ void updateGamepads (ImGuiIO &io_) std::make_tuple (std::ref (cpad.dy), ImGuiNavInput_LStickDown, -0.3f, -0.9f), }; + // read left joystick from circle pad hidCircleRead (&cpad); for (auto const &[in, out, min, max] : analogMapping) { auto const value = in / static_cast (0x9C); - auto const v = std::min (1.0f, (value - min) / (max - min)); - io_.NavInputs[out] = std::max (io_.NavInputs[out], v); + io_.NavInputs[out] = std::clamp ((value - min) / (max - min), 0.0f, 1.0f); } } } @@ -117,17 +136,21 @@ bool imgui::ctru::init () { ImGuiIO &io = ImGui::GetIO (); + // disable imgui.ini file io.IniFilename = nullptr; + // setup config flags io.ConfigFlags |= ImGuiConfigFlags_IsTouchScreen; io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; + // setup platform backend io.BackendFlags |= ImGuiBackendFlags_HasGamepad; + io.BackendPlatformName = "3DS"; - io.BackendPlatformName = "3ds"; - + // disable mouse cursor io.MouseDrawCursor = false; + // clipboard callbacks io.SetClipboardTextFn = setClipboardText; io.GetClipboardTextFn = getClipboardText; io.ClipboardUserData = nullptr; @@ -139,19 +162,21 @@ void imgui::ctru::newFrame () { ImGuiIO &io = ImGui::GetIO (); + // check that font was built IM_ASSERT (io.Fonts->IsBuilt () && "Font atlas not built! It is generally built by the renderer back-end. Missing call " "to renderer _NewFrame() function?"); + // setup display metrics io.DisplaySize = ImVec2 (SCREEN_WIDTH * 2.0f, SCREEN_HEIGHT * 2.0f); io.DisplayFramebufferScale = ImVec2 (0.5f, 0.5f); - // Setup time step - static auto const start = svcGetSystemTick (); + // time step + static auto const start = platform::steady_clock::now (); static auto prev = start; - auto const now = svcGetSystemTick (); + auto const now = platform::steady_clock::now (); - io.DeltaTime = (now - prev) / static_cast (SYSCLOCK_ARM11); + io.DeltaTime = std::chrono::duration (now - prev).count (); prev = now; updateTouch (io); diff --git a/source/3ds/imgui_ctru.h b/source/3ds/imgui_ctru.h index 2684b72..7e76289 100644 --- a/source/3ds/imgui_ctru.h +++ b/source/3ds/imgui_ctru.h @@ -24,9 +24,12 @@ namespace imgui { namespace ctru { +/// \brief Initialize 3ds platform bool init (); +/// \brief Deinitialize 3ds platform void exit (); +/// \brief Prepare 3ds for a new frame void newFrame (); } } diff --git a/source/3ds/platform.cpp b/source/3ds/platform.cpp index c1e4f5d..3733266 100644 --- a/source/3ds/platform.cpp +++ b/source/3ds/platform.cpp @@ -41,33 +41,45 @@ namespace { -constexpr auto STACK_SIZE = 32768; -constexpr auto SOCU_ALIGN = 0x1000; +/// \brief Thread stack size +constexpr auto STACK_SIZE = 0x8000; +/// \brief soc:u buffer alignment +constexpr auto SOCU_ALIGN = 0x1000; +/// \brief soc:u buffer size constexpr auto SOCU_BUFFERSIZE = 0x100000; static_assert (SOCU_BUFFERSIZE % SOCU_ALIGN == 0); +/// \brief Whether soc:u is active bool s_socuActive = false; +/// \brief soc:u buffer u32 *s_socuBuffer = nullptr; +/// \brief Texture atlas C3D_Tex s_gfxTexture; +/// \brief Texture atlas metadata Tex3DS_Texture s_gfxT3x; +/// \brief Start network void startNetwork () { + // check if already active if (s_socuActive) return; + // get wifi status std::uint32_t wifi = 0; if (R_FAILED (ACU_GetWifiStatus (&wifi)) || !wifi) return; + // allocate soc:u buffer if (!s_socuBuffer) s_socuBuffer = static_cast (::memalign (SOCU_ALIGN, SOCU_BUFFERSIZE)); if (!s_socuBuffer) return; + // initialize soc:u service if (R_FAILED (socInit (s_socuBuffer, SOCU_BUFFERSIZE))) return; @@ -75,27 +87,34 @@ void startNetwork () Log::info ("Wifi connected\n"); } +/// \brief Draw citro3d logo void drawLogo () { + // get citro3d logo subtexture auto subTex = Tex3DS_GetSubTexture (s_gfxT3x, gfx_c3dlogo_idx); + // get framebuffer metrics ImGuiIO &io = ImGui::GetIO (); auto const screenWidth = io.DisplaySize.x; auto const screenHeight = io.DisplaySize.y; auto const logoWidth = subTex->width / io.DisplayFramebufferScale.x; auto const logoHeight = subTex->height / io.DisplayFramebufferScale.y; + // calculate top screen coords auto const x1 = (screenWidth - logoWidth) / 2.0f; auto const x2 = x1 + logoWidth; auto const y1 = (screenHeight / 2.0f - logoHeight) / 2.0f; auto const y2 = y1 + logoHeight; + // calculate uv coords auto const uv1 = ImVec2 (subTex->left, subTex->top); auto const uv2 = ImVec2 (subTex->right, subTex->bottom); + // draw to top screen ImGui::GetBackgroundDrawList ()->AddImage ( &s_gfxTexture, ImVec2 (x1, y1), ImVec2 (x2, y2), uv1, uv2); + // draw to bottom screen ImGui::GetBackgroundDrawList ()->AddImage (&s_gfxTexture, ImVec2 (x1, y1 + screenHeight / 2.0f), ImVec2 (x2, y2 + screenHeight / 2.0f), @@ -103,6 +122,7 @@ void drawLogo () uv2); } +/// \brief Draw status void drawStatus () { constexpr unsigned batteryLevels[] = { @@ -121,6 +141,7 @@ void drawStatus () gfx_wifi3_idx, }; + // get battery charging state or level static u8 charging = 0; static u8 level = 5; PTMU_GetBatteryChargeState (&charging); @@ -136,33 +157,43 @@ void drawStatus () auto const screenWidth = io.DisplaySize.x; + // calculate battery icon metrics auto const battery = Tex3DS_GetSubTexture (s_gfxT3x, charging ? gfx_batteryCharge_idx : batteryLevels[level]); auto const batteryWidth = battery->width / io.DisplayFramebufferScale.x; auto const batteryHeight = battery->height / io.DisplayFramebufferScale.y; + // calculate battery icon position auto const p1 = ImVec2 (screenWidth - batteryWidth, 0.0f); auto const p2 = ImVec2 (screenWidth, batteryHeight); + // calculate battery icon uv coords auto const uv1 = ImVec2 (battery->left, battery->top); auto const uv2 = ImVec2 (battery->right, battery->bottom); + // draw battery icon ImGui::GetForegroundDrawList ()->AddImage (&s_gfxTexture, p1, p2, uv1, uv2); + // get wifi strength auto const wifiStrength = osGetWifiStrength (); + // calculate wifi icon metrics auto const wifi = Tex3DS_GetSubTexture (s_gfxT3x, wifiLevels[wifiStrength]); auto const wifiWidth = wifi->width / io.DisplayFramebufferScale.x; auto const wifiHeight = wifi->height / io.DisplayFramebufferScale.y; + // calculate wifi icon position auto const p3 = ImVec2 (p1.x - wifiWidth - 4.0f, 0.0f); auto const p4 = ImVec2 (p1.x - 4.0f, wifiHeight); + // calculate wifi icon uv coords auto const uv3 = ImVec2 (wifi->left, wifi->top); auto const uv4 = ImVec2 (wifi->right, wifi->bottom); + // draw wifi icon ImGui::GetForegroundDrawList ()->AddImage (&s_gfxTexture, p3, p4, uv3, uv4); + // draw current timestamp char buffer[64]; auto const now = std::time (nullptr); std::strftime (buffer, sizeof (buffer), "%H:%M:%S", std::localtime (&now)); @@ -173,6 +204,7 @@ void drawStatus () bool platform::init () { + // enable New 3DS speedup osSetSpeedupEnable (true); acInit (); @@ -187,18 +219,13 @@ bool platform::init () std::setvbuf (stderr, nullptr, _IOLBF, 0); #endif - IMGUI_CHECKVERSION (); - ImGui::CreateContext (); - if (!imgui::ctru::init ()) - { - ImGui::DestroyContext (); return false; - } imgui::citro3d::init (); { + // load texture atlas fs::File file; if (!file.open ("romfs:/gfx.t3x")) svcBreak (USERBREAK_PANIC); @@ -206,9 +233,9 @@ bool platform::init () s_gfxT3x = Tex3DS_TextureImportStdio (file, &s_gfxTexture, nullptr, true); if (!s_gfxT3x) svcBreak (USERBREAK_PANIC); - } - C3D_TexSetFilter (&s_gfxTexture, GPU_LINEAR, GPU_LINEAR); + C3D_TexSetFilter (&s_gfxTexture, GPU_LINEAR, GPU_LINEAR); + } return true; } @@ -247,8 +274,6 @@ void platform::exit () Tex3DS_TextureFree (s_gfxT3x); C3D_TexDelete (&s_gfxTexture); - ImGui::DestroyContext (); - imgui::citro3d::exit (); imgui::ctru::exit (); @@ -262,6 +287,7 @@ void platform::exit () } /////////////////////////////////////////////////////////////////////////// +/// \brief Platform thread pimpl class platform::Thread::privateData_t { public: @@ -271,6 +297,8 @@ public: threadFree (thread); } + /// \brief Parameterized constructor + /// \param func_ Thread entry point privateData_t (std::function func_) : thread (nullptr) { s32 priority = 0x30; @@ -280,13 +308,18 @@ public: assert (thread); } + /// \brief Underlying thread entry point + /// \param arg_ Thread pimpl object static void threadFunc (void *const arg_) { + // call passed-in entry point auto const t = static_cast (arg_); t->func (); } + /// \brief Underlying thread ::Thread thread = nullptr; + /// \brief Thread entry point std::function func; }; @@ -323,9 +356,11 @@ void platform::Thread::sleep (std::chrono::milliseconds const timeout_) } /////////////////////////////////////////////////////////////////////////// +/// \brief Platform mutex pimpl class platform::Mutex::privateData_t { public: + /// \brief Underlying mutex LightLock mutex; }; diff --git a/source/3ds/vshader.v.pica b/source/3ds/vshader.v.pica index 5b9955e..e7f6f71 100644 --- a/source/3ds/vshader.v.pica +++ b/source/3ds/vshader.v.pica @@ -18,26 +18,28 @@ ; You should have received a copy of the GNU General Public License ; along with this program. If not, see . -; Example PICA200 vertex shader +; ImGui PICA200 vertex shader -; Uniforms +; uniforms +; Projection matrix .fvec proj[4] -; Constants +; constants +; [1.0, 0.0, 1.0/255.0, 0.0] .constf constants(1.0, 0.0, 0.00392156862745, 0.0) -; Outputs +; outputs .out outPos position .out outUv texcoord0 .out outColor color -; Inputs (defined as aliases for convenience) +; inputs (defined as aliases for convenience) .alias inPos v0 .alias inUv v1 .alias inColor v2 .proc main - ; Force inPos.z = 0.0, inPos.w = 1.0 + ; force inPos.z = 0.0, inPos.w = 1.0 mov r0.xy, inPos.xy mov r0.zw, constants.yx @@ -56,6 +58,6 @@ ; outColor = inColor mov outColor, r1 - ; We're finished + ; we're finished end .end diff --git a/source/fs.cpp b/source/fs.cpp index 43c4c61..f580f46 100644 --- a/source/fs.cpp +++ b/source/fs.cpp @@ -45,28 +45,34 @@ std::string fs::printSize (std::uint64_t const size_) // clang-format on }) { + // get the integral portion of the number auto const whole = size_ / bin; if (size_ >= 100 * bin) { + // >= 100, print xxxXiB std::sprintf (buffer, "%" PRIu64 "%s", whole, name); return buffer; } + // get the fractional portion of the number auto const frac = size_ - whole * bin; if (size_ >= 10 * bin) { + // >= 10, print xx.xXiB std::sprintf (buffer, "%" PRIu64 ".%" PRIu64 "%s", whole, frac * 10 / bin, name); return buffer; } if (size_ >= 1000 * (bin / KiB)) { + // >= 1000 of lesser bin, print x.xxXiB std::sprintf (buffer, "%" PRIu64 ".%02" PRIu64 "%s", whole, frac * 100 / bin, name); return buffer; } } - std::sprintf (buffer, "%" PRIu64, size_); + // < 1KiB, just print the number + std::sprintf (buffer, "%" PRIu64 "B", size_); return buffer; } @@ -207,5 +213,6 @@ void fs::Dir::close () struct dirent *fs::Dir::read () { + errno = 0; return ::readdir (m_dp.get ()); } diff --git a/source/ftpServer.cpp b/source/ftpServer.cpp index 935bc62..6ff09ed 100644 --- a/source/ftpServer.cpp +++ b/source/ftpServer.cpp @@ -36,6 +36,7 @@ using namespace std::chrono_literals; #ifndef _3DS +/// \todo Investigate multithreading on 3DS #define MULTITHREADED 1 #else #define MULTITHREADED 0 @@ -43,17 +44,24 @@ using namespace std::chrono_literals; namespace { -auto const s_startTime = std::chrono::system_clock::to_time_t (std::chrono::system_clock::now ()); +/// \brief Application start time +auto const s_startTime = std::time (nullptr); + +/// \brief Mutex for s_freeSpace platform::Mutex s_lock; + +/// \brief Free space string std::string s_freeSpace; } /////////////////////////////////////////////////////////////////////////// FtpServer::~FtpServer () { +#if MULTITHREADED m_quit = true; m_thread.join (); +#endif } FtpServer::FtpServer (std::uint16_t const port_) @@ -118,6 +126,7 @@ void FtpServer::draw () ImGui::Separator (); #ifdef _3DS + // Fill rest of top screen window ImGui::BeginChild ("Logs", ImVec2 (0, 0), false, ImGuiWindowFlags_HorizontalScrollbar); #else ImGui::BeginChild ("Logs", ImVec2 (0, 200), false, ImGuiWindowFlags_HorizontalScrollbar); @@ -214,6 +223,7 @@ void FtpServer::handleStopButton () void FtpServer::loop () { { + // poll listen socket auto const lock = std::scoped_lock (m_lock); if (m_socket) { @@ -227,6 +237,7 @@ void FtpServer::loop () } } + // remove dead sessions for (auto it = std::begin (m_sessions); it != std::end (m_sessions);) { auto const &session = *it; @@ -236,9 +247,11 @@ void FtpServer::loop () ++it; } + // poll sessions if (!m_sessions.empty ()) FtpSession::poll (m_sessions); #if MULTITHREADED + // avoid busy polling in background thread else platform::Thread::sleep (16ms); #endif @@ -246,6 +259,7 @@ void FtpServer::loop () void FtpServer::threadFunc () { + // bind log for this thread Log::bind (m_log); while (!m_quit) diff --git a/source/ftpSession.cpp b/source/ftpSession.cpp index 1f9239f..c71d424 100644 --- a/source/ftpSession.cpp +++ b/source/ftpSession.cpp @@ -55,8 +55,13 @@ using namespace std::chrono_literals; namespace { +/// \brief Parse command +/// \param buffer_ Buffer to parse +/// \param size_ Size of buffer +/// \returns {delimiterPos, nextPos} std::pair parseCommand (char *const buffer_, std::size_t const size_) { + // look for \r\n or \n delimiter auto const end = &buffer_[size_]; for (auto p = buffer_; p < end; ++p) { @@ -70,6 +75,9 @@ std::pair parseCommand (char *const buffer_, std::size_t const s return {nullptr, nullptr}; } +/// \brief Decode path +/// \param buffer_ Buffer to decode +/// \param size_ Size of buffer void decodePath (char *const buffer_, std::size_t const size_) { auto const end = &buffer_[size_]; @@ -81,6 +89,9 @@ void decodePath (char *const buffer_, std::size_t const size_) } } +/// \brief Encode path +/// \param buffer_ Buffer to encode +/// \param quotes_ Whether to encode quotes std::string encodePath (std::string_view const buffer_, bool const quotes_ = false) { // check if the buffer has \n @@ -104,13 +115,16 @@ std::string encodePath (std::string_view const buffer_, bool const quotes_ = fal } while (p); } + // if nothing needs escaping, return it as-is if (!lf && !numQuotes) return std::string (buffer_); + // reserve output buffer std::string path (buffer_.size () + numQuotes, '\0'); auto in = buffer_.data (); auto out = path.data (); + // encode into the output buffer while (in < end) { if (*in == '\n') @@ -132,6 +146,8 @@ std::string encodePath (std::string_view const buffer_, bool const quotes_ = fal return path; } +/// \brief Get parent directory name of a path +/// \param path_ Path to get parent of std::string dirName (std::string_view const path_) { // remove last path component @@ -142,6 +158,8 @@ std::string dirName (std::string_view const path_) return dir; } +/// \brief Resolve path +/// \param path_ Path to resolve std::string resolvePath (std::string_view const path_) { assert (!path_.empty ()); @@ -209,6 +227,9 @@ std::string resolvePath (std::string_view const path_) return outPath; } +/// \brief Build path from a parent and child +/// \param cwd_ Parent directory +/// \param args_ Child component std::string buildPath (std::string_view const cwd_, std::string_view const args_) { // absolute path @@ -222,6 +243,9 @@ std::string buildPath (std::string_view const cwd_, std::string_view const args_ return std::string (cwd_) + '/' + std::string (args_); } +/// \brief Build resolved path from a parent and child +/// \param cwd_ Parent directory +/// \param args_ Child component std::string buildResolvedPath (std::string_view const cwd_, std::string_view const args_) { return resolvePath (buildPath (cwd_, args_)); diff --git a/source/ioBuffer.cpp b/source/ioBuffer.cpp index 2eb42d0..a900148 100644 --- a/source/ioBuffer.cpp +++ b/source/ioBuffer.cpp @@ -29,6 +29,7 @@ IOBuffer::~IOBuffer () = default; IOBuffer::IOBuffer (std::size_t const size_) : m_buffer (std::make_unique (size_)), m_size (size_) { + assert (size_ > 0); } char *IOBuffer::freeArea () const diff --git a/source/pc/KHR/khrplatform.h b/source/linux/KHR/khrplatform.h similarity index 100% rename from source/pc/KHR/khrplatform.h rename to source/linux/KHR/khrplatform.h diff --git a/source/pc/glad.c b/source/linux/glad.c similarity index 100% rename from source/pc/glad.c rename to source/linux/glad.c diff --git a/source/pc/glad/glad.h b/source/linux/glad/glad.h similarity index 100% rename from source/pc/glad/glad.h rename to source/linux/glad/glad.h diff --git a/source/pc/imgui_impl_glfw.cpp b/source/linux/imgui_impl_glfw.cpp similarity index 100% rename from source/pc/imgui_impl_glfw.cpp rename to source/linux/imgui_impl_glfw.cpp diff --git a/source/pc/imgui_impl_glfw.h b/source/linux/imgui_impl_glfw.h similarity index 100% rename from source/pc/imgui_impl_glfw.h rename to source/linux/imgui_impl_glfw.h diff --git a/source/pc/imgui_impl_opengl3.cpp b/source/linux/imgui_impl_opengl3.cpp similarity index 100% rename from source/pc/imgui_impl_opengl3.cpp rename to source/linux/imgui_impl_opengl3.cpp diff --git a/source/pc/imgui_impl_opengl3.h b/source/linux/imgui_impl_opengl3.h similarity index 100% rename from source/pc/imgui_impl_opengl3.h rename to source/linux/imgui_impl_opengl3.h diff --git a/source/pc/platform.cpp b/source/linux/platform.cpp similarity index 87% rename from source/pc/platform.cpp rename to source/linux/platform.cpp index 3cc737f..a4046cf 100644 --- a/source/pc/platform.cpp +++ b/source/linux/platform.cpp @@ -36,8 +36,13 @@ namespace { +/// \brief GLFW main window std::unique_ptr s_mainWindow (nullptr, glfwDestroyWindow); +/// \brief Window resize callback +/// \param window_ GLFW window +/// \param width_ New window width +/// \param height_ New window height void windowResize (GLFWwindow *const window_, int const width_, int const height_) { (void)window_; @@ -49,6 +54,13 @@ void windowResize (GLFWwindow *const window_, int const width_, int const height } #ifndef NDEBUG +/// \brief GL log callback +/// \param source_ Message source +/// \param type_ Message type +/// \param id_ Message id +/// \param severity_ Message severity +/// \param length_ Message length +/// \param userParam_ User parameter void logCallback (GLenum const source_, GLenum const type_, GLuint const id_, @@ -63,19 +75,21 @@ void logCallback (GLenum const source_, (void)severity_; (void)length_; (void)userParam_; - // std::fprintf (stderr, "%s\n", message_); + std::fprintf (stderr, "%s\n", message_); } #endif } bool platform::init () { + // initialize GLFW if (!glfwInit ()) { std::fprintf (stderr, "Failed to initialize GLFW\n"); return false; } + // use OpenGL 4.3 Core Profile glfwWindowHint (GLFW_CONTEXT_VERSION_MAJOR, 4); glfwWindowHint (GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint (GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); @@ -87,6 +101,7 @@ bool platform::init () glfwWindowHint (GLFW_DEPTH_BITS, 24); glfwWindowHint (GLFW_STENCIL_BITS, 8); + // create GLFW window s_mainWindow.reset (glfwCreateWindow (1280, 720, "Test Game", nullptr, nullptr)); if (!s_mainWindow) { @@ -95,12 +110,14 @@ bool platform::init () return false; } + // enable vsync glfwSwapInterval (1); // create context glfwMakeContextCurrent (s_mainWindow.get ()); glfwSetFramebufferSizeCallback (s_mainWindow.get (), windowResize); + // load OpenGL if (!gladLoadGL ()) { std::fprintf (stderr, "gladLoadGL: failed\n"); @@ -134,12 +151,10 @@ bool platform::init () std::printf ("Renderer: %s\n", glGetString (GL_RENDERER)); std::printf ("OpenGL Version: %s\n", glGetString (GL_VERSION)); - IMGUI_CHECKVERSION (); - ImGui::CreateContext (); - ImGui_ImplGlfw_InitForOpenGL (s_mainWindow.get (), true); ImGui_ImplOpenGL3_Init ("#version 150"); + // disable imgui.ini file ImGuiIO &io = ImGui::GetIO (); io.IniFilename = nullptr; @@ -169,10 +184,6 @@ void platform::render () { ImGui::Render (); - int width; - int height; - glfwGetFramebufferSize (s_mainWindow.get (), &width, &height); - glViewport (0, 0, width, height); glClearColor (0.45f, 0.55f, 0.60f, 1.00f); glClear (GL_COLOR_BUFFER_BIT); @@ -183,22 +194,24 @@ void platform::render () void platform::exit () { - ImGui::DestroyContext (); - s_mainWindow.reset (); glfwTerminate (); } /////////////////////////////////////////////////////////////////////////// +/// \brief Platform thread pimpl class platform::Thread::privateData_t { public: privateData_t () = default; + /// \brief Parameterized constructor + /// \param func_ Thread entry point privateData_t (std::function func_) : thread (func_) { } + /// \brief Underlying thread object std::thread thread; }; @@ -235,9 +248,11 @@ void platform::Thread::sleep (std::chrono::milliseconds const timeout_) } /////////////////////////////////////////////////////////////////////////// +/// \brief Platform mutex pimpl class platform::Mutex::privateData_t { public: + /// \brief Underlying mutex std::mutex mutex; }; diff --git a/source/log.cpp b/source/log.cpp index 5b83cfb..2a26dfa 100644 --- a/source/log.cpp +++ b/source/log.cpp @@ -27,12 +27,17 @@ namespace { #ifdef _3DS +/// \brief Maximum number of log messages to keep constexpr auto MAX_LOGS = 250; #else +/// \brief Maximum number of log messages to keep constexpr auto MAX_LOGS = 10000; #endif + +/// \brief Bound log thread_local WeakLog s_log; +/// \brief Message prefix static char const *const s_prefix[] = { [Log::DEBUG] = "[DEBUG]", [Log::INFO] = "[INFO]", @@ -59,19 +64,11 @@ void Log::draw () } static ImVec4 const s_colors[] = { - [Log::DEBUG] = ImVec4 (1.0f, 1.0f, 0.4f, 1.0f), - [Log::INFO] = ImVec4 (1.0f, 1.0f, 1.0f, 1.0f), - [Log::ERROR] = ImVec4 (1.0f, 0.4f, 0.4f, 1.0f), - [Log::COMMAND] = ImVec4 (0.4f, 1.0f, 0.4f, 1.0f), - [Log::RESPONSE] = ImVec4 (0.4f, 1.0f, 1.0f, 1.0f), - }; - - static char const *const s_prefix[] = { - [Log::DEBUG] = "[DEBUG]", - [Log::INFO] = "[INFO]", - [Log::ERROR] = "[ERROR]", - [Log::COMMAND] = "[COMMAND]", - [Log::RESPONSE] = "[RESPONSE]", + [Log::DEBUG] = ImVec4 (1.0f, 1.0f, 0.4f, 1.0f), // yellow + [Log::INFO] = ImVec4 (1.0f, 1.0f, 1.0f, 1.0f), // white + [Log::ERROR] = ImVec4 (1.0f, 0.4f, 0.4f, 1.0f), // red + [Log::COMMAND] = ImVec4 (0.4f, 1.0f, 0.4f, 1.0f), // green + [Log::RESPONSE] = ImVec4 (0.4f, 1.0f, 1.0f, 1.0f), // cyan }; for (auto const &message : m_messages) @@ -83,7 +80,7 @@ void Log::draw () ImGui::PopStyleColor (); } - // auto scroll if scroll bar is at end + // auto-scroll if scroll bar is at end if (ImGui::GetScrollY () >= ImGui::GetScrollMaxY ()) ImGui::SetScrollHereY (1.0f); } @@ -183,6 +180,7 @@ void Log::log (Level const level_, std::string_view const message_) auto msg = std::string (message_); for (auto &c : msg) { + // replace nul-characters with ? to avoid truncation if (c == '\0') c = '?'; } diff --git a/source/main.cpp b/source/main.cpp index cfc6703..2104aff 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -29,14 +29,21 @@ int main (int argc_, char *argv_[]) { + IMGUI_CHECKVERSION (); + ImGui::CreateContext (); + if (!platform::init ()) + { + ImGui::DestroyContext (); return EXIT_FAILURE; + } auto &style = ImGui::GetStyle (); style.WindowRounding = 0.0f; #ifdef _3DS - style.Colors[ImGuiCol_WindowBg].w = 0.5f; + // citro3d logo doesn't quite show with the default transparency + style.Colors[ImGuiCol_WindowBg].w = 0.8f; #endif auto server = FtpServer::create (5000); @@ -48,5 +55,7 @@ int main (int argc_, char *argv_[]) platform::render (); } + server.reset (); platform::exit (); + ImGui::DestroyContext (); } diff --git a/source/sockAddr.cpp b/source/sockAddr.cpp index 26fecec..9a86b72 100644 --- a/source/sockAddr.cpp +++ b/source/sockAddr.cpp @@ -23,6 +23,7 @@ #include #include +#include #include /////////////////////////////////////////////////////////////////////////// @@ -51,18 +52,24 @@ SockAddr::SockAddr (struct sockaddr const &addr_) std::memcpy (&m_addr, &addr_, sizeof (struct sockaddr_in6)); break; #endif + + default: + std::abort (); + break; } } SockAddr::SockAddr (struct sockaddr_in const &addr_) : SockAddr (reinterpret_cast (addr_)) { + assert (m_addr.ss_family == AF_INET); } #ifndef _3DS SockAddr::SockAddr (struct sockaddr_in6 const &addr_) : SockAddr (reinterpret_cast (addr_)) { + assert (m_addr.ss_family == AF_INET6); } #endif @@ -111,9 +118,11 @@ std::uint16_t SockAddr::port () const case AF_INET6: return ntohs (reinterpret_cast (&m_addr)->sin6_port); #endif - } - return 0; + default: + std::abort (); + break; + } } char const *SockAddr::name (char *buffer_, std::size_t size_) const @@ -133,9 +142,11 @@ char const *SockAddr::name (char *buffer_, std::size_t size_) const buffer_, size_); #endif - } - return nullptr; + default: + std::abort (); + break; + } } char const *SockAddr::name () const diff --git a/source/socket.cpp b/source/socket.cpp index 3c88ecf..c66b673 100644 --- a/source/socket.cpp +++ b/source/socket.cpp @@ -38,8 +38,10 @@ Socket::~Socket () { if (m_listening) Log::info ("Stop listening on [%s]:%u\n", m_sockName.name (), m_sockName.port ()); + if (m_connected) Log::info ("Closing connection to [%s]:%u\n", m_peerName.name (), m_peerName.port ()); + if (::close (m_fd) != 0) Log::error ("close: %s\n", std::strerror (errno)); } @@ -66,7 +68,7 @@ UniqueSocket Socket::accept () if (fd < 0) { Log::error ("accept: %s\n", std::strerror (errno)); - return {nullptr, {}}; + return nullptr; } Log::info ("Accepted connection from [%s]:%u\n", addr.name (), addr.port ()); @@ -112,6 +114,7 @@ bool Socket::bind (SockAddr const &addr_) if (addr_.port () == 0) { + // get socket name due to request for ephemeral port socklen_t addrLen = sizeof (struct sockaddr_storage); if (::getsockname (m_fd, m_sockName, &addrLen) != 0) Log::error ("getsockname: %s\n", std::strerror (errno)); @@ -200,7 +203,7 @@ bool Socket::setNonBlocking (bool const nonBlocking_) if (::fcntl (m_fd, F_SETFL, flags) != 0) { - Log::error ("fcntl(F_SETFL): %s\n", std::strerror (errno)); + Log::error ("fcntl(F_SETFL, %d): %s\n", flags, std::strerror (errno)); return false; } @@ -212,7 +215,8 @@ bool Socket::setReuseAddress (bool const reuse_) int reuse = reuse_; if (::setsockopt (m_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof (reuse)) != 0) { - Log::error ("setsockopt(SO_REUSEADDR, %u): %s\n", reuse_, std::strerror (errno)); + Log::error ( + "setsockopt(SO_REUSEADDR, %s): %s\n", reuse_ ? "yes" : "no", std::strerror (errno)); return false; } diff --git a/source/nx/imgui_deko3d.cpp b/source/switch/imgui_deko3d.cpp similarity index 72% rename from source/nx/imgui_deko3d.cpp rename to source/switch/imgui_deko3d.cpp index 4e86617..32bee3e 100644 --- a/source/nx/imgui_deko3d.cpp +++ b/source/switch/imgui_deko3d.cpp @@ -48,27 +48,39 @@ namespace { -constexpr auto LOGO_WIDTH = 500; +/// \brief deko3d logo width +constexpr auto LOGO_WIDTH = 500; +/// \brief deko3d logo height constexpr auto LOGO_HEIGHT = 493; +/// \brief Number of framebuffers constexpr auto FB_NUM = 2u; -constexpr auto CMDBUF_SIZE = 1024 * 1024; -constexpr auto DATABUF_SIZE = 1024 * 1024; +/// \brief Command buffer size +constexpr auto CMDBUF_SIZE = 1024 * 1024; +/// \brief Data buffer size +constexpr auto DATABUF_SIZE = 1024 * 1024; +/// \brief Index buffer size constexpr auto INDEXBUF_SIZE = 1024 * 1024; +/// \brief Image buffer size constexpr auto IMAGEBUF_SIZE = 16 * 1024 * 1024; +/// \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; }; -constexpr std::array VertexAttribState = { +/// \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}, @@ -76,58 +88,95 @@ constexpr std::array VertexAttribState = { // clang-format on }; -constexpr std::array VertexBufferState = { +/// \brief Vertex buffer state +constexpr std::array VERTEX_BUFFER_STATE = { DkVtxBufferState{sizeof (ImDrawVert), 0}, }; +/// \brief deko3d device dk::UniqueDevice s_device; +/// \brief Depth buffer memblock dk::UniqueMemBlock s_depthMemBlock; +/// \brief Depth buffer image dk::Image s_depthBuffer; +/// \brief Framebuffer memblock dk::UniqueMemBlock s_fbMemBlock; +/// \brief Framebuffer images dk::Image s_frameBuffers[FB_NUM]; +/// \brief Font image dk::Image s_fontTexture; +/// \brief deko3d logo image dk::Image s_logoTexture; +/// \brief deko3d swapchain dk::UniqueSwapchain s_swapchain; +/// \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 dk::UniqueMemBlock s_vtxMemBlock[FB_NUM]; +/// \brief Index data memblock dk::UniqueMemBlock s_idxMemBlock[FB_NUM]; +/// \brief Command buffer memblock dk::UniqueMemBlock s_cmdMemBlock[FB_NUM]; +/// \brief Command buffers dk::UniqueCmdBuf s_cmdBuf[FB_NUM]; +/// \brief Image memblock dk::UniqueMemBlock s_imageMemBlock; +/// \brief Image/Sampler descriptor memblock dk::UniqueMemBlock s_descriptorMemBlock; +/// \brief deko3d queue dk::UniqueQueue s_queue; -constexpr auto MAX_SAMPLERS = 1; -constexpr auto MAX_IMAGES = 2; +/// \brief Maximum number of samplers +constexpr auto MAX_SAMPLERS = 1; +/// \brief Maximum number of images +constexpr auto MAX_IMAGES = 2; +/// \brief Sample descriptors dk::SamplerDescriptor *s_samplerDescriptors = nullptr; -dk::ImageDescriptor *s_imageDescriptors = nullptr; +/// \brief Image descriptors +dk::ImageDescriptor *s_imageDescriptors = nullptr; +/// \brief Currently bound image descriptor std::uintptr_t s_boundDescriptor = 0; -unsigned s_width = 0; +/// \brief Framebuffer width +unsigned s_width = 0; +/// \brief Framebuffer height unsigned s_height = 0; +/// \brief Align 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); } +/// \brief Rebuild swapchain +/// \param width_ Framebuffer width +/// \param height_ Framebuffer height +/// \note This assumes the first call is the largest a framebuffer will ever be 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) @@ -138,6 +187,7 @@ void rebuildSwapchain (unsigned const width_, unsigned const height_) auto const depthAlign = depthLayout.getAlignment (); auto const depthSize = depthLayout.getSize (); + // create depth buffer memblock if (!s_depthMemBlock) { s_depthMemBlock = dk::MemBlockMaker{s_device, @@ -148,6 +198,7 @@ void rebuildSwapchain (unsigned const width_, unsigned const height_) s_depthBuffer.initialize (depthLayout, s_depthMemBlock, 0); + // create framebuffer image layout dk::ImageLayout fbLayout; dk::ImageLayoutMaker{s_device} .setFlags ( @@ -159,6 +210,7 @@ void rebuildSwapchain (unsigned const width_, unsigned const height_) auto const fbAlign = fbLayout.getAlignment (); auto const fbSize = fbLayout.getSize (); + // create framebuffer memblock if (!s_fbMemBlock) { s_fbMemBlock = dk::MemBlockMaker{s_device, @@ -167,6 +219,7 @@ void rebuildSwapchain (unsigned const width_, unsigned const height_) .create (); } + // initialize swapchain images std::array swapchainImages; for (unsigned i = 0; i < FB_NUM; ++i) { @@ -174,18 +227,26 @@ void rebuildSwapchain (unsigned const width_, unsigned const height_) s_frameBuffers[i].initialize (fbLayout, s_fbMemBlock, i * fbSize); } + // create swapchain s_swapchain = dk::SwapchainMaker{s_device, nwindowGetDefault (), swapchainImages}.create (); } +/// \brief Load shader code void loadShaders () { + /// \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; @@ -199,14 +260,18 @@ void loadShaders () 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, @@ -214,6 +279,7 @@ void loadShaders () return sum_ + align (file_.size, DK_SHADER_CODE_ALIGNMENT); }); + // create shader code memblock s_codeMemBlock = dk::MemBlockMaker{s_device, align (codeSize, DK_MEMBLOCK_ALIGNMENT)} .setFlags (DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached | DkMemBlockFlags_Code) @@ -222,6 +288,7 @@ void loadShaders () 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; @@ -245,13 +312,18 @@ void loadShaders () } } +/// \brief Setup render state +/// \param slot_ Swapchain slot +/// \param drawData_ Data to draw +/// \param width_ Framebuffer width +/// \param height_ Framebuffer height DkCmdList setupRenderState (int const slot_, 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 + // 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; @@ -262,6 +334,7 @@ DkCmdList setupRenderState (int const slot_, VertUBO vertUBO; vertUBO.projMtx = glm::orthoRH_ZO (L, R, B, T, -1.0f, 1.0f); + // create command buffer to initialize/reset render state s_cmdBuf[slot_].setViewports (0, DkViewport{0.0f, 0.0f, width_, height_}); s_cmdBuf[slot_].bindShaders (DkStageFlag_GraphicsMask, {&s_shaders[0], &s_shaders[1]}); s_cmdBuf[slot_].bindUniformBuffer (DkStage_Vertex, @@ -286,8 +359,8 @@ DkCmdList setupRenderState (int const slot_, DkBlendFactor_InvSrcAlpha, DkBlendFactor_InvSrcAlpha, DkBlendFactor_Zero)); - s_cmdBuf[slot_].bindVtxAttribState (VertexAttribState); - s_cmdBuf[slot_].bindVtxBufferState (VertexBufferState); + s_cmdBuf[slot_].bindVtxAttribState (VERTEX_ATTRIB_STATE); + s_cmdBuf[slot_].bindVtxBufferState (VERTEX_BUFFER_STATE); return s_cmdBuf[slot_].finishList (); } @@ -295,17 +368,21 @@ DkCmdList setupRenderState (int const slot_, void imgui::deko3d::init () { - // Setup back-end capabilities flags + // setup back-end capabilities flags ImGuiIO &io = ImGui::GetIO (); io.BackendRendererName = "deko3d"; io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; + + // defer initialization to first newFrame () } void imgui::deko3d::exit () { + // wait for queue to be idle s_queue.waitIdle (); + // clean up all of the deko3d objects s_queue = nullptr; s_descriptorMemBlock = nullptr; s_imageMemBlock = nullptr; @@ -331,12 +408,16 @@ void imgui::deko3d::newFrame () if (s_device) return; + // create deko3d device s_device = dk::DeviceMaker{}.create (); + // initialize swapchain with maximum resolution rebuildSwapchain (1920, 1080); + // load shader code loadShaders (); + // create UBO memblock s_uboMemBlock = dk::MemBlockMaker{s_device, align (align (sizeof (VertUBO), DK_UNIFORM_BUF_ALIGNMENT) + align (sizeof (FragUBO), DK_UNIFORM_BUF_ALIGNMENT), @@ -344,32 +425,38 @@ void imgui::deko3d::newFrame () .setFlags (DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached) .create (); + // create memblocks for each framebuffer slot for (std::size_t i = 0; i < FB_NUM; ++i) { + // create vertex data memblock s_vtxMemBlock[i] = dk::MemBlockMaker{s_device, align (DATABUF_SIZE, DK_MEMBLOCK_ALIGNMENT)} .setFlags (DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached) .create (); + // create index data memblock s_idxMemBlock[i] = dk::MemBlockMaker{s_device, align (INDEXBUF_SIZE, DK_MEMBLOCK_ALIGNMENT)} .setFlags (DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached) .create (); + // create command buffer memblock s_cmdMemBlock[i] = dk::MemBlockMaker{s_device, 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 queue s_queue = dk::QueueMaker{s_device}.setFlags (DkQueueFlags_Graphics).create (); + // create image memblock s_imageMemBlock = dk::MemBlockMaker{s_device, align (IMAGEBUF_SIZE, DK_MEMBLOCK_ALIGNMENT)} .setFlags (DkMemBlockFlags_GpuCached | DkMemBlockFlags_Image) .create (); - // Build texture atlas + // get texture atlas ImGuiIO &io = ImGui::GetIO (); io.Fonts->SetTexID (nullptr); unsigned char *pixels; @@ -377,12 +464,14 @@ void imgui::deko3d::newFrame () int height; io.Fonts->GetTexDataAsAlpha8 (&pixels, &width, &height); + // create memblock for transfer dk::UniqueMemBlock memBlock = dk::MemBlockMaker{s_device, align (width * height, DK_MEMBLOCK_ALIGNMENT)} .setFlags (DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached) .create (); std::memcpy (memBlock.getCpuAddr (), pixels, width * height); + // 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); @@ -391,17 +480,22 @@ void imgui::deko3d::newFrame () .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]); + // initialize sampler descriptor s_samplerDescriptors[0].initialize ( dk::Sampler{} .setFilter (DkFilter_Linear, DkFilter_Linear) .setWrapMode (DkWrapMode_ClampToEdge, DkWrapMode_ClampToEdge, DkWrapMode_ClampToEdge)); + // use command buffer 0 for initialization auto &cmdBuf = s_cmdBuf[0]; + + // initialize texture atlas image layout dk::ImageLayout layout; dk::ImageLayoutMaker{s_device} .setFlags (0) @@ -409,21 +503,25 @@ void imgui::deko3d::newFrame () .setDimensions (width, height) .initialize (layout); + // initialize font texture atlas image descriptor s_fontTexture.initialize (layout, s_imageMemBlock, 0); s_imageDescriptors[0].initialize (s_fontTexture); + // copy font texture atlas to image view dk::ImageView imageView{s_fontTexture}; cmdBuf.copyBufferToImage ({memBlock.getGpuAddr ()}, imageView, {0, 0, 0, width, height, 1}); + // bind image/sampler descriptors cmdBuf.bindSamplerDescriptorSet (s_descriptorMemBlock.getGpuAddr (), MAX_SAMPLERS); cmdBuf.bindImageDescriptorSet ( s_descriptorMemBlock.getGpuAddr () + MAX_SAMPLERS * sizeof (dk::SamplerDescriptor), MAX_IMAGES); + // submit commands while we get the next image ready to transfer s_queue.submitCommands (cmdBuf.finishList ()); - s_queue.waitIdle (); { + // read the deko3d logo auto const path = "romfs:/deko3d.rgba.zst"; struct stat st; @@ -456,10 +554,13 @@ void imgui::deko3d::newFrame () std::abort (); } - memBlock = dk::MemBlockMaker{s_device, align (size, DK_MEMBLOCK_ALIGNMENT)} - .setFlags (DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached) - .create (); + // create memblock for transfer + dk::UniqueMemBlock memBlock = + dk::MemBlockMaker{s_device, align (size, DK_MEMBLOCK_ALIGNMENT)} + .setFlags (DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached) + .create (); + // decompress into transfer memblock auto const decoded = ZSTD_decompress (memBlock.getCpuAddr (), size, buffer.data (), st.st_size); if (ZSTD_isError (decoded)) @@ -468,6 +569,7 @@ void imgui::deko3d::newFrame () std::abort (); } + // initialize deko3d logo texture image layout dk::ImageLayout layout; dk::ImageLayoutMaker{s_device} .setFlags (0) @@ -475,32 +577,41 @@ void imgui::deko3d::newFrame () .setDimensions (LOGO_WIDTH, LOGO_HEIGHT) .initialize (layout); + // initialize deko3d logo texture image descriptor auto const offset = align (width * height, DK_IMAGE_LINEAR_STRIDE_ALIGNMENT); s_logoTexture.initialize (layout, s_imageMemBlock, offset); s_imageDescriptors[1].initialize (s_logoTexture); + // copy deko3d logo texture to image view dk::ImageView imageView{s_logoTexture}; cmdBuf.copyBufferToImage ( {memBlock.getGpuAddr ()}, imageView, {0, 0, 0, LOGO_WIDTH, LOGO_HEIGHT, 1}); + // submit commands to transfer deko3d logo texture s_queue.submitCommands (cmdBuf.finishList ()); + + // wait for commands to complete before releasing memblocks s_queue.waitIdle (); } + // reset command buffer cmdBuf.clear (); } void imgui::deko3d::render () { + // 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; + // check if we need to rebuild the swapchain if (width != s_width || height != s_height) { s_width = width; @@ -510,9 +621,11 @@ void imgui::deko3d::render () rebuildSwapchain (width, height); } + // get image from queue auto const slot = s_queue.acquireImage (s_swapchain); s_cmdBuf[slot].clear (); + // bind frame/depth buffers and clear them dk::ImageView colorTarget{s_frameBuffers[slot]}; dk::ImageView depthTarget{s_depthBuffer}; s_cmdBuf[slot].bindRenderTargets (&colorTarget, &depthTarget); @@ -520,21 +633,22 @@ void imgui::deko3d::render () s_cmdBuf[slot].clearDepthStencil (true, 1.0f, 0xFF, 0); s_queue.submitCommands (s_cmdBuf[slot].finishList ()); - // Setup desired render state + // setup desired render state auto const setupCmd = setupRenderState (slot, drawData, width, height); s_queue.submitCommands (setupCmd); + // start with bogus descriptor binding so it'll be updated before first draw call s_boundDescriptor = ~static_cast (0); - // Will project scissor/clipping rectangles into framebuffer space + // 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)) { - s_vtxMemBlock[slot] = nullptr; s_vtxMemBlock[slot] = dk::MemBlockMaker{s_device, align (drawData->TotalVtxCount * sizeof (ImDrawVert), DK_MEMBLOCK_ALIGNMENT)} @@ -542,9 +656,9 @@ void imgui::deko3d::render () .create (); } + // check if we need to grow index data memblock if (s_idxMemBlock[slot].getSize () < drawData->TotalIdxCount * sizeof (ImDrawIdx)) { - s_idxMemBlock[slot] = nullptr; s_idxMemBlock[slot] = dk::MemBlockMaker{s_device, align (drawData->TotalIdxCount * sizeof (ImDrawIdx), DK_MEMBLOCK_ALIGNMENT)} @@ -552,20 +666,24 @@ void imgui::deko3d::render () .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) == 2); s_cmdBuf[slot].bindVtxBuffer (0, gpuVtx, sizeVtx); s_cmdBuf[slot].bindIdxBuffer (DkIdxFormat_Uint16, gpuIdx); - // Render command lists + // render command lists std::size_t offsetVtx = 0; std::size_t offsetIdx = 0; for (int i = 0; i < drawData->CmdListsCount; ++i) @@ -575,6 +693,7 @@ void imgui::deko3d::render () 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"); @@ -582,6 +701,7 @@ void imgui::deko3d::render () continue; } + // double check that we don't overrun index data memblock if (sizeIdx - offsetIdx < idxSize) { std::fprintf (stderr, "Not enough index buffer\n"); @@ -589,6 +709,7 @@ void imgui::deko3d::render () continue; } + // copy vertex/index data into memblocks std::memcpy (cpuVtx + offsetVtx, cmdList.VtxBuffer.Data, vtxSize); std::memcpy (cpuIdx + offsetIdx, cmdList.IdxBuffer.Data, idxSize); @@ -596,9 +717,10 @@ void imgui::deko3d::render () { if (cmd.UserCallback) { + // submit commands to preserve ordering s_queue.submitCommands (s_cmdBuf[slot].finishList ()); - // User callback, registered via ImDrawList::AddCallback() + // 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) @@ -608,53 +730,65 @@ void imgui::deko3d::render () } else { - // Project scissor/clipping rectangles into framebuffer space + // 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; - if (clip.x < width && clip.y < height && clip.z >= 0.0f && clip.w >= 0.0f) + 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 + s_cmdBuf[slot].setScissors ( + 0, DkScissor{clip.x, clip.y, clip.z - clip.x, clip.w - clip.y}); + + // get image descriptor + auto const descriptor = reinterpret_cast (cmd.TextureId); + if (descriptor >= MAX_IMAGES) + continue; + + // check if we need to bind a new texture + if (descriptor != s_boundDescriptor) { - if (clip.x < 0.0f) - clip.x = 0.0f; - if (clip.y < 0.0f) - clip.y = 0.0f; + s_boundDescriptor = descriptor; - s_cmdBuf[slot].setScissors ( - 0, DkScissor{clip.x, clip.y, clip.z - clip.x, clip.w - clip.y}); + // bind the new texture + s_cmdBuf[slot].bindTextures ( + DkStage_Fragment, 0, dkMakeTextureHandle (descriptor, 0)); - auto const descriptor = reinterpret_cast (cmd.TextureId); - if (descriptor >= MAX_IMAGES) - continue; + // check if this is the font texture atlas image descriptor + FragUBO fragUBO; + fragUBO.font = (descriptor == 0); - if (descriptor != s_boundDescriptor) - { - s_boundDescriptor = descriptor; - - s_cmdBuf[slot].bindTextures ( - DkStage_Fragment, 0, dkMakeTextureHandle (descriptor, 0)); - - FragUBO fragUBO; - fragUBO.font = (descriptor == 0); - - s_cmdBuf[slot].pushConstants ( - s_uboMemBlock.getGpuAddr () + - align (sizeof (VertUBO), DK_UNIFORM_BUF_ALIGNMENT), - align (sizeof (FragUBO), DK_UNIFORM_BUF_ALIGNMENT), - 0, - sizeof (FragUBO), - &fragUBO); - } - - s_cmdBuf[slot].drawIndexed (DkPrimitive_Triangles, - cmd.ElemCount, - 1, - cmd.IdxOffset + offsetIdx / sizeof (ImDrawIdx), - cmd.VtxOffset + offsetVtx / sizeof (ImDrawVert), - 0); + // update fragment shader UBO + s_cmdBuf[slot].pushConstants ( + s_uboMemBlock.getGpuAddr () + + align (sizeof (VertUBO), DK_UNIFORM_BUF_ALIGNMENT), + align (sizeof (FragUBO), DK_UNIFORM_BUF_ALIGNMENT), + 0, + sizeof (FragUBO), + &fragUBO); } + + // draw the draw list + s_cmdBuf[slot].drawIndexed (DkPrimitive_Triangles, + cmd.ElemCount, + 1, + cmd.IdxOffset + offsetIdx / sizeof (ImDrawIdx), + cmd.VtxOffset + offsetVtx / sizeof (ImDrawVert), + 0); } } @@ -662,10 +796,14 @@ void imgui::deko3d::render () offsetIdx += idxSize; } + // wait for fragments to be completed before discarding depth/stencil buffer s_cmdBuf[slot].barrier (DkBarrier_Fragments, 0); s_cmdBuf[slot].discardDepthStencil (); + + // submit final commands s_queue.submitCommands (s_cmdBuf[slot].finishList ()); + // present image s_queue.presentImage (s_swapchain, slot); } diff --git a/source/nx/imgui_deko3d.h b/source/switch/imgui_deko3d.h similarity index 100% rename from source/nx/imgui_deko3d.h rename to source/switch/imgui_deko3d.h diff --git a/source/nx/imgui_fsh.glsl b/source/switch/imgui_fsh.glsl similarity index 96% rename from source/nx/imgui_fsh.glsl rename to source/switch/imgui_fsh.glsl index 8398b4f..100b1b6 100644 --- a/source/nx/imgui_fsh.glsl +++ b/source/switch/imgui_fsh.glsl @@ -33,6 +33,7 @@ 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 diff --git a/source/nx/imgui_nx.cpp b/source/switch/imgui_nx.cpp similarity index 95% rename from source/nx/imgui_nx.cpp rename to source/switch/imgui_nx.cpp index ae1ef48..3df452c 100644 --- a/source/nx/imgui_nx.cpp +++ b/source/switch/imgui_nx.cpp @@ -23,6 +23,7 @@ #include "imgui.h" #include "fs.h" +#include "platform.h" #include @@ -35,23 +36,32 @@ using namespace std::chrono_literals; namespace { +/// \brief Font atlas cache file constexpr auto FONT_ATLAS_BIN = "ftpd-font.bin"; -bool s_mouseJustPressed[IM_ARRAYSIZE (ImGuiIO::MouseDown)]; - -std::chrono::high_resolution_clock::time_point s_lastMouseUpdate; +/// \brief Last mouse update timestamp +std::chrono::steady_clock::time_point s_lastMouseUpdate; +/// \brief Whether application is focused bool s_focused = true; -float s_width = 1280.0f; + +/// \brief Framebuffer width +float s_width = 1280.0f; +/// \brief Framebuffer height float s_height = 720.0f; +/// \brief Whether to show mouse float s_showMouse = false; +/// \brief Mouse position ImVec2 s_mousePos = ImVec2 (0.0f, 0.0f); +/// \brief Clipboard std::string s_clipboard; +/// \brief Applet hook cookie AppletHookCookie s_appletHookCookie; +/// \brief System font glyph ranges ImWchar const nxFontRanges[] = { // clang-format off 0x0020, 0x007e, 0x00a0, 0x017f, 0x0192, 0x0192, 0x01c0, 0x01c0, @@ -1189,12 +1199,16 @@ ImWchar const nxFontRanges[] = { // clang-format on }; +/// \brief Handle applet hook +/// \param hook_ Callback reason +/// \param param_ User param void handleAppletHook (AppletHookType const hook_, void *const param_) { (void)param_; switch (hook_) { case AppletHookType_OnFocusState: + // grab focus state s_focused = (appletGetFocusState () == AppletFocusState_Focused); break; @@ -1203,15 +1217,18 @@ void handleAppletHook (AppletHookType const hook_, void *const param_) { default: case AppletOperationMode_Handheld: + // use handheld mode resolution (720p) s_width = 1280.0f; s_height = 720.0f; break; case AppletOperationMode_Docked: + // use docked mode resolution (1080p) #if 0 s_width = 1920.0f; s_height = 1080.0f; #else + /// \todo check if we'd rather use framebuffer scale s_width = 1280.0f; s_height = 720.0f; #endif @@ -1224,30 +1241,43 @@ void handleAppletHook (AppletHookType const hook_, void *const param_) } } +/// \brief Get clipboard text callback +/// \param userData_ User data char const *getClipboardText (void *const userData_) { (void)userData_; return s_clipboard.c_str (); } +/// \brief Set clipboard text callback +/// \param userData_ User data +/// \param text_ Clipboard text void setClipboardText (void *const userData_, char const *const text_) { (void)userData_; s_clipboard = text_; } +/// \brief Move mouse cursor +/// \param io_ ImGui IO +/// \param pos_ New mouse position +/// \param force_ Whether to ignore prior mouse position void moveMouse (ImGuiIO &io_, ImVec2 const &pos_, bool const force_ = false) { - auto const now = std::chrono::high_resolution_clock::now (); + // get update timestamp + auto const now = std::chrono::steady_clock::now (); + // check if the mouse position has been updated if (!force_ && pos_.x == s_mousePos.x && pos_.y == s_mousePos.y) { + // stop displaying mouse cursor after inactivity timeout if (now - s_lastMouseUpdate > 1s) s_showMouse = false; return; } + // update mouse position s_showMouse = true; s_lastMouseUpdate = now; s_mousePos = pos_; @@ -1255,27 +1285,27 @@ void moveMouse (ImGuiIO &io_, ImVec2 const &pos_, bool const force_ = false) io_.MousePos = s_mousePos; } +/// \brief Update mouse buttons +/// \param io_ ImGui IO void updateMouseButtons (ImGuiIO &io_) { + // read mouse buttons auto const buttons = hidMouseButtonsHeld (); for (std::size_t i = 0; i < IM_ARRAYSIZE (io_.MouseDown); ++i) { - // If a mouse press event came, always pass it as "mouse held this frame", so we don't miss - // click-release events that are shorter than 1 frame. - io_.MouseDown[i] = s_mouseJustPressed[i] || (buttons & BIT (i)); - s_mouseJustPressed[i] = false; + io_.MouseDown[i] = buttons & BIT (i); + // force mouse cursor to show on click if (io_.MouseDown[i]) moveMouse (io_, s_mousePos, true); } } +/// \brief Update mouse position +/// \param io_ ImGui IO void updateMousePos (ImGuiIO &io_) { - if (!s_focused) - return; - MousePosition pos; hidMouseRead (&pos); @@ -1286,25 +1316,30 @@ void updateMousePos (ImGuiIO &io_) io_, ImVec2 (s_mousePos.x + 2.0f * pos.velocityX, s_mousePos.y + 2.0f * pos.velocityY)); } +/// \brief Update touch position +/// \param io_ ImGui IO void updateTouch (ImGuiIO &io_) { - if (!s_focused) - return; - + // read touch positions auto const touchCount = hidTouchCount (); if (touchCount < 1) return; + // use first touch position touchPosition pos; hidTouchRead (&pos, 0); + // set mouse position to touch point; force hide mouse cursor moveMouse (io_, ImVec2 (pos.px, pos.py)); io_.MouseDown[0] = true; s_showMouse = false; } +/// \brief Update gamepad inputs +/// \param io_ ImGui IO void updateGamepads (ImGuiIO &io_) { + // clear navigation inputs std::memset (io_.NavInputs, 0, sizeof (io_.NavInputs)); auto const buttonMapping = { @@ -1322,6 +1357,7 @@ void updateGamepads (ImGuiIO &io_) std::make_pair (KEY_DLEFT, ImGuiNavInput_DpadLeft), }; + // read buttons from primary controller auto const keys = hidKeysHeld (CONTROLLER_P1_AUTO); for (auto const &[in, out] : buttonMapping) { @@ -1329,7 +1365,7 @@ void updateGamepads (ImGuiIO &io_) io_.NavInputs[out] = 1.0f; } - // Use ZR/ZL as Mouse0/Mouse1, respectively + // use ZR/ZL as left-click/right-click, respectively if (keys & KEY_ZR) { io_.MouseDown[0] = true; @@ -1341,6 +1377,7 @@ void updateGamepads (ImGuiIO &io_) moveMouse (io_, s_mousePos, true); } + // update joystick JoystickPosition js; auto const analogMapping = { std::make_tuple (std::ref (js.dx), ImGuiNavInput_LStickLeft, -0.3f, -0.9f), @@ -1349,15 +1386,15 @@ void updateGamepads (ImGuiIO &io_) std::make_tuple (std::ref (js.dy), ImGuiNavInput_LStickDown, -0.3f, -0.9f), }; + // read left joystick from primary controller hidJoystickRead (&js, CONTROLLER_P1_AUTO, JOYSTICK_LEFT); for (auto const &[in, out, min, max] : analogMapping) { auto const value = in / static_cast (JOYSTICK_MAX); - auto const v = std::min (1.0f, (value - min) / (max - min)); - io_.NavInputs[out] = std::max (io_.NavInputs[out], v); + io_.NavInputs[out] = std::clamp ((value - min) / (max - min), 0.0f, 1.0f); } - // Use right stick as mouse + // use right stick as mouse auto scale = 5.0f; if (keys & KEY_L) scale = 1.0f; @@ -1365,37 +1402,41 @@ void updateGamepads (ImGuiIO &io_) scale = 20.0f; hidJoystickRead (&js, CONTROLLER_P1_AUTO, JOYSTICK_RIGHT); + // move mouse moveMouse (io_, ImVec2 (s_mousePos.x + js.dx / static_cast (JOYSTICK_MAX) * scale, s_mousePos.y - js.dy / static_cast (JOYSTICK_MAX) * scale)); } +/// \brief Update keyboard inputs +/// \param io_ ImGui IO void updateKeyboard (ImGuiIO &io_) { io_.KeyCtrl = hidKeyboardModifierHeld (static_cast (KBD_MOD_LCTRL | KBD_MOD_RCTRL)); + io_.KeyShift = hidKeyboardModifierHeld ( static_cast (KBD_MOD_LSHIFT | KBD_MOD_RSHIFT)); + io_.KeyAlt = hidKeyboardModifierHeld (static_cast (KBD_MOD_LALT | KBD_MOD_RALT)); + io_.KeySuper = hidKeyboardModifierHeld (static_cast (KBD_MOD_LMETA | KBD_MOD_RMETA)); for (int i = 0; i < 256; ++i) io_.KeysDown[i] = hidKeyboardHeld (static_cast (i)); - - if (!io_.WantTextInput) - return; - - // io_.AddInputCharacter (c); } +/// \brief Load font atlas cache from disk bool loadFontAtlas () { + // open font atlas fs::File fp; if (!fp.open (FONT_ATLAS_BIN)) return false; + // initialize font atlas auto const atlas = ImGui::GetIO ().Fonts; atlas->Clear (); atlas->TexWidth = fp.read (); @@ -1405,9 +1446,11 @@ bool loadFontAtlas () atlas->TexPixelsAlpha8 = reinterpret_cast (IM_ALLOC (atlas->TexWidth * atlas->TexHeight)); + // read pixel data if (!fp.readAll (atlas->TexPixelsAlpha8, atlas->TexWidth * atlas->TexHeight)) return false; + // initialize font config ImFontConfig config; config.FontData = nullptr; config.FontDataSize = 0; @@ -1428,23 +1471,28 @@ bool loadFontAtlas () config.EllipsisChar = 0x2026; std::memset (config.Name, 0, sizeof (config.Name)); + // create font auto const font = IM_NEW (ImFont); config.DstFont = font; + // add config and font to atlas atlas->ConfigData.push_back (config); atlas->Fonts.push_back (font); atlas->CustomRectIds[0] = atlas->AddCustomRectRegular (0x80000000, 108 * 2 + 1, 27); atlas->CustomRects[0].X = 0; atlas->CustomRects[0].Y = 0; + // read some font metrics font->FallbackAdvanceX = fp.read (); font->FontSize = fp.read (); + // decode glyph metadata auto const glyphCount = fp.read (); for (unsigned i = 0; i < glyphCount; ++i) { ImFontGlyph glyph; + // read glyph metadata glyph.Codepoint = fp.read (); glyph.AdvanceX = fp.read (); glyph.X0 = fp.read (); @@ -1456,17 +1504,21 @@ bool loadFontAtlas () glyph.U1 = fp.read (); glyph.V1 = fp.read (); + // add glyph to font font->Glyphs.push_back (glyph); font->MetricsTotalSurface += static_cast ((glyph.U1 - glyph.U0) * atlas->TexWidth + 1.99f) * static_cast ((glyph.V1 - glyph.V0) * atlas->TexHeight + 1.99f); } + // build font lookup table font->BuildLookupTable (); + // read display offsets font->DisplayOffset.x = fp.read (); font->DisplayOffset.y = fp.read (); + // finalize font atlas font->ContainerAtlas = atlas; font->ConfigData = &atlas->ConfigData[0]; font->ConfigDataCount = 1; @@ -1479,6 +1531,7 @@ bool loadFontAtlas () return true; } +/// \brief Store font atlas cache to disk bool saveFontAtlas () { auto const atlas = ImGui::GetIO ().Fonts; @@ -1488,24 +1541,30 @@ bool saveFontAtlas () int height; atlas->GetTexDataAsAlpha8 (&pixels, &width, &height); + // create font atlas cache fs::File fp; if (!fp.open (FONT_ATLAS_BIN, "wb")) return false; + // save atlas dimensions fp.write (width); fp.write (height); + // write pixel data if (!fp.writeAll (pixels, width * height)) return false; auto const font = atlas->ConfigData[0].DstFont; + // write some font metrics fp.write (font->FallbackAdvanceX); fp.write (font->FontSize); + // encode glyph metadata fp.write (font->Glyphs.size ()); for (auto const &glyph : font->Glyphs) { + // write glyph metadata fp.write (glyph.Codepoint); fp.write (glyph.AdvanceX); fp.write (glyph.X0); @@ -1518,6 +1577,7 @@ bool saveFontAtlas () fp.write (glyph.V1); } + // write remaining font metrics fp.write (font->DisplayOffset.x); fp.write (font->DisplayOffset.y); fp.write (font->Ascent); @@ -1529,44 +1589,71 @@ bool saveFontAtlas () bool imgui::nx::init () { - u64 languageCode; - auto rc = setInitialize (); - if (R_FAILED (rc)) - return false; + ImGuiIO &io = ImGui::GetIO (); - rc = setGetSystemLanguage (&languageCode); - if (R_FAILED (rc)) + if (!loadFontAtlas ()) { + // get system language + u64 languageCode; + auto rc = setInitialize (); + if (R_FAILED (rc)) + return false; + + rc = setGetSystemLanguage (&languageCode); + if (R_FAILED (rc)) + { + setExit (); + return false; + } setExit (); - return false; + + // get fonts for system language + std::vector fonts (PlSharedFontType_Total); + s32 numFonts = 0; + rc = plGetSharedFont (languageCode, fonts.data (), fonts.size (), &numFonts); + if (R_FAILED (rc)) + return false; + fonts.resize (numFonts); + + // add fonts + ImFontConfig config; + config.MergeMode = false; + config.FontDataOwnedByAtlas = false; + for (auto const &font : fonts) + { + io.Fonts->AddFontFromMemoryTTF (font.address, font.size, 14.0f, &config, nxFontRanges); + config.MergeMode = true; + } + + // build font atlas + io.Fonts->Flags |= ImFontAtlasFlags_NoPowerOfTwoHeight; + io.Fonts->Build (); + + // save font atlas + saveFontAtlas (); } - setExit (); - - std::vector fonts (PlSharedFontType_Total); - s32 numFonts = 0; - rc = plGetSharedFont (languageCode, fonts.data (), fonts.size (), &numFonts); - if (R_FAILED (rc)) - return false; - fonts.resize (numFonts); + else + std::printf ("Loaded font atlas from disk\n"); + // initialize applet hooks appletSetFocusHandlingMode (AppletFocusHandlingMode_NoSuspend); appletHook (&s_appletHookCookie, handleAppletHook, nullptr); handleAppletHook (AppletHookType_OnFocusState, nullptr); handleAppletHook (AppletHookType_OnOperationMode, nullptr); - ImGuiIO &io = ImGui::GetIO (); - + // disable imgui.ini file io.IniFilename = nullptr; + // setup config flags io.ConfigFlags |= ImGuiConfigFlags_IsTouchScreen; io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + // setup platform backend io.BackendFlags |= ImGuiBackendFlags_HasGamepad; + io.BackendPlatformName = "Switch"; - io.BackendPlatformName = "switch"; - - // Keyboard mapping. ImGui will use those indices to peek into the io.KeysDown[] array. + // keyboard mapping. ImGui will use those indices to peek into the io.KeysDown[] array. io.KeyMap[ImGuiKey_Tab] = KBD_TAB; io.KeyMap[ImGuiKey_LeftArrow] = KBD_LEFT; io.KeyMap[ImGuiKey_RightArrow] = KBD_RIGHT; @@ -1590,66 +1677,58 @@ bool imgui::nx::init () io.KeyMap[ImGuiKey_Y] = KBD_Y; io.KeyMap[ImGuiKey_Z] = KBD_Z; + // initially disable mouse cursor io.MouseDrawCursor = false; + // clipboard callbacks io.SetClipboardTextFn = setClipboardText; io.GetClipboardTextFn = getClipboardText; io.ClipboardUserData = nullptr; - if (!loadFontAtlas ()) - { - ImFontConfig config; - config.MergeMode = false; - config.FontDataOwnedByAtlas = false; - - for (auto const &font : fonts) - { - io.Fonts->AddFontFromMemoryTTF (font.address, font.size, 14.0f, &config, nxFontRanges); - config.MergeMode = true; - } - io.Fonts->Flags |= ImFontAtlasFlags_NoPowerOfTwoHeight; - io.Fonts->Build (); - - saveFontAtlas (); - } - else - std::printf ("Loaded font atlas from disk\n"); - return true; } void imgui::nx::newFrame () { ImGuiIO &io = ImGui::GetIO (); + + // check that font was built IM_ASSERT (io.Fonts->IsBuilt () && "Font atlas not built! It is generally built by the renderer back-end. Missing call " "to renderer _NewFrame() function?"); + // setup display metrics io.DisplaySize = ImVec2 (s_width, s_height); io.DisplayFramebufferScale = ImVec2 (1.0f, 1.0f); - // Setup time step - static auto const start = std::chrono::high_resolution_clock::now (); + // time step + static auto const start = platform::steady_clock::now (); static auto prev = start; - auto const now = std::chrono::high_resolution_clock::now (); + auto const now = platform::steady_clock::now (); io.DeltaTime = std::chrono::duration (now - prev).count (); prev = now; - updateMouseButtons (io); - updateMousePos (io); - updateTouch (io); - updateGamepads (io); - updateKeyboard (io); + if (s_focused) + { + // update inputs + updateMouseButtons (io); + updateMousePos (io); + updateTouch (io); + updateGamepads (io); + updateKeyboard (io); + } + // whether to draw mouse cursor io.MouseDrawCursor = s_showMouse; - // Clamp mouse to screen + // 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); } void imgui::nx::exit () { + // deinitialize applet hooks appletUnhook (&s_appletHookCookie); } diff --git a/source/nx/imgui_nx.h b/source/switch/imgui_nx.h similarity index 100% rename from source/nx/imgui_nx.h rename to source/switch/imgui_nx.h diff --git a/source/nx/imgui_vsh.glsl b/source/switch/imgui_vsh.glsl similarity index 100% rename from source/nx/imgui_vsh.glsl rename to source/switch/imgui_vsh.glsl diff --git a/source/nx/init.c b/source/switch/init.c similarity index 91% rename from source/nx/init.c rename to source/switch/init.c index 6f084e1..3df6113 100644 --- a/source/nx/init.c +++ b/source/switch/init.c @@ -23,9 +23,11 @@ #include #ifndef NDEBUG +/// \brief nxlink socket fd static int s_fd = -1; #endif +/// \brief Socket initialization configuration static SocketInitConfig const s_socketInitConfig = { .bsdsockets_version = 1, @@ -43,10 +45,13 @@ static SocketInitConfig const s_socketInitConfig = { .bsd_service_type = BsdServiceType_User, }; +/// \brief Number of FS sessions u32 __nx_fs_num_sessions = 3; +/// \brief Called before main () void userAppInit () { + // disable immediate app close appletLockExit (); romfsInit (); diff --git a/source/nx/platform.cpp b/source/switch/platform.cpp similarity index 93% rename from source/nx/platform.cpp rename to source/switch/platform.cpp index 0c6b1ed..27418b2 100644 --- a/source/nx/platform.cpp +++ b/source/switch/platform.cpp @@ -32,14 +32,8 @@ bool platform::init () { - IMGUI_CHECKVERSION (); - ImGui::CreateContext (); - if (!imgui::nx::init ()) - { - ImGui::DestroyContext (); return false; - } imgui::deko3d::init (); @@ -77,20 +71,22 @@ void platform::exit () { imgui::nx::exit (); imgui::deko3d::exit (); - - ImGui::DestroyContext (); } /////////////////////////////////////////////////////////////////////////// +/// \brief Platform thread pimpl class platform::Thread::privateData_t { public: privateData_t () = default; + /// \brief Parameterized constructor + /// \param func_ Thread entry point privateData_t (std::function func_) : thread (func_) { } + /// \brief Underlying thread std::thread thread; }; @@ -128,12 +124,16 @@ void platform::Thread::sleep (std::chrono::milliseconds const timeout_) /////////////////////////////////////////////////////////////////////////// #define USE_STD_MUTEX 1 + +/// \brief Platform mutex pimpl class platform::Mutex::privateData_t { public: #if USE_STD_MUTEX + /// \brief Underlying mutex std::mutex mutex; #else + /// \brief Underlying mutex ::Mutex mutex; #endif }; diff --git a/gfx.switch/deko3d.png b/switch/gfx/deko3d.png similarity index 100% rename from gfx.switch/deko3d.png rename to switch/gfx/deko3d.png