From 561d0458ba9eb0b6ba234c899a388c3a725c0729 Mon Sep 17 00:00:00 2001 From: Adubbz Date: Tue, 7 Jul 2020 17:21:30 +1000 Subject: [PATCH] Implemented a system updater homebrew (titled Daybreak) --- .gitignore | 3 + troposphere/Makefile | 2 +- troposphere/daybreak/Makefile | 282 ++++++ troposphere/daybreak/icon.jpg | Bin 0 -> 22123 bytes troposphere/daybreak/source/ams_su.c | 158 ++++ troposphere/daybreak/source/ams_su.h | 51 ++ troposphere/daybreak/source/assert.hpp | 23 + troposphere/daybreak/source/main.cpp | 257 ++++++ troposphere/daybreak/source/service_guard.h | 56 ++ troposphere/daybreak/source/ui.cpp | 966 ++++++++++++++++++++ troposphere/daybreak/source/ui.hpp | 240 +++++ troposphere/daybreak/source/ui_util.cpp | 192 ++++ troposphere/daybreak/source/ui_util.hpp | 39 + 13 files changed, 2268 insertions(+), 1 deletion(-) create mode 100644 troposphere/daybreak/Makefile create mode 100644 troposphere/daybreak/icon.jpg create mode 100644 troposphere/daybreak/source/ams_su.c create mode 100644 troposphere/daybreak/source/ams_su.h create mode 100644 troposphere/daybreak/source/assert.hpp create mode 100644 troposphere/daybreak/source/main.cpp create mode 100644 troposphere/daybreak/source/service_guard.h create mode 100644 troposphere/daybreak/source/ui.cpp create mode 100644 troposphere/daybreak/source/ui.hpp create mode 100644 troposphere/daybreak/source/ui_util.cpp create mode 100644 troposphere/daybreak/source/ui_util.hpp diff --git a/.gitignore b/.gitignore index 54b77b4ef..c8c423006 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,9 @@ *.x86_64 *.hex +# Deko3d shaders +*.dksh + # Switch Executables *.nso *.nro diff --git a/troposphere/Makefile b/troposphere/Makefile index 2be61ea0a..25adf0da2 100644 --- a/troposphere/Makefile +++ b/troposphere/Makefile @@ -1,4 +1,4 @@ -APPLICATIONS := reboot_to_payload +APPLICATIONS := daybreak reboot_to_payload SUBFOLDERS := $(APPLICATIONS) diff --git a/troposphere/daybreak/Makefile b/troposphere/daybreak/Makefile new file mode 100644 index 000000000..8753aca8f --- /dev/null +++ b/troposphere/daybreak/Makefile @@ -0,0 +1,282 @@ +#--------------------------------------------------------------------------------- +.SUFFIXES: +#--------------------------------------------------------------------------------- + +ifeq ($(strip $(DEVKITPRO)),) +$(error "Please set DEVKITPRO in your environment. export DEVKITPRO=/devkitpro") +endif + +TOPDIR ?= $(CURDIR) +include $(DEVKITPRO)/libnx/switch_rules + +#--------------------------------------------------------------------------------- +# TARGET is the name of the output +# BUILD is the directory where object files & intermediate files will be placed +# SOURCES is a list of directories containing source code +# DATA is a list of directories containing data files +# INCLUDES is a list of directories containing header files +# ROMFS is the directory containing data to be added to RomFS, relative to the Makefile (Optional) +# +# NO_ICON: if set to anything, do not use icon. +# NO_NACP: if set to anything, no .nacp file is generated. +# APP_TITLE is the name of the app stored in the .nacp file (Optional) +# APP_AUTHOR is the author of the app stored in the .nacp file (Optional) +# APP_VERSION is the version of the app stored in the .nacp file (Optional) +# APP_TITLEID is the titleID of the app stored in the .nacp file (Optional) +# ICON is the filename of the icon (.jpg), relative to the project folder. +# If not set, it attempts to use one of the following (in this order): +# - .jpg +# - icon.jpg +# - /default_icon.jpg +# +# CONFIG_JSON is the filename of the NPDM config file (.json), relative to the project folder. +# If not set, it attempts to use one of the following (in this order): +# - .json +# - config.json +# If a JSON file is provided or autodetected, an ExeFS PFS0 (.nsp) is built instead +# of a homebrew executable (.nro). This is intended to be used for sysmodules. +# NACP building is skipped as well. +#--------------------------------------------------------------------------------- +TARGET := daybreak +BUILD := build +SOURCES := source nanovg/shaders +DATA := data +INCLUDES := include ../include +ROMFS := romfs + +# Output folders for autogenerated files in romfs +OUT_SHADERS := shaders + +APP_TITLE := Daybreak +APP_AUTHOR := Atmosphere-NX +APP_VERSION := 1.0.0 + +#--------------------------------------------------------------------------------- +# options for code generation +#--------------------------------------------------------------------------------- +ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE + +CFLAGS := -g -Wall -O2 -ffunction-sections \ + $(ARCH) $(DEFINES) + +CFLAGS += $(INCLUDE) -D__SWITCH__ + +CXXFLAGS := $(CFLAGS) -std=gnu++17 -fno-exceptions -fno-rtti + +ASFLAGS := -g $(ARCH) +LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) + +LIBS := -lnanovg -ldeko3d -lnx + +#--------------------------------------------------------------------------------- +# list of directories containing libraries, this must be the top level containing +# include and lib +#--------------------------------------------------------------------------------- +LIBDIRS := $(PORTLIBS) $(LIBNX) $(CURDIR)/nanovg/ + +#--------------------------------------------------------------------------------- +# no real need to edit anything past this point unless you need to add additional +# rules for different file extensions +#--------------------------------------------------------------------------------- +ifneq ($(BUILD),$(notdir $(CURDIR))) +#--------------------------------------------------------------------------------- + +export OUTPUT := $(CURDIR)/$(TARGET) +export TOPDIR := $(CURDIR) + +export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \ + $(foreach dir,$(DATA),$(CURDIR)/$(dir)) + +export DEPSDIR := $(CURDIR)/$(BUILD) + +SUBFOLDERS := nanovg + +TOPTARGETS := all clean + +$(TOPTARGETS): $(SUBFOLDERS) + +$(SUBFOLDERS): + $(MAKE) -C $@ $(MAKECMDGOALS) + +CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) +CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp))) +SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) +GLSLFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.glsl))) +BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*))) + +#--------------------------------------------------------------------------------- +# use CXX for linking C++ projects, CC for standard C +#--------------------------------------------------------------------------------- +ifeq ($(strip $(CPPFILES)),) +#--------------------------------------------------------------------------------- + export LD := $(CC) +#--------------------------------------------------------------------------------- +else +#--------------------------------------------------------------------------------- + export LD := $(CXX) +#--------------------------------------------------------------------------------- +endif +#--------------------------------------------------------------------------------- + +export OFILES_BIN := $(addsuffix .o,$(BINFILES)) +export OFILES_SRC := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o) +export OFILES := $(OFILES_BIN) $(OFILES_SRC) +export HFILES_BIN := $(addsuffix .h,$(subst .,_,$(BINFILES))) +export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) + +ifneq ($(strip $(ROMFS)),) + ROMFS_TARGETS := + ROMFS_FOLDERS := + ifneq ($(strip $(OUT_SHADERS)),) + ROMFS_SHADERS := $(ROMFS)/$(OUT_SHADERS) + ROMFS_TARGETS += $(patsubst %.glsl, $(ROMFS_SHADERS)/%.dksh, $(GLSLFILES)) + ROMFS_FOLDERS += $(ROMFS_SHADERS) + endif + + export ROMFS_DEPS := $(foreach file,$(ROMFS_TARGETS),$(CURDIR)/$(file)) +endif + +export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ + $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ + -I$(CURDIR)/$(BUILD) + +ifeq ($(strip $(CONFIG_JSON)),) + jsons := $(wildcard *.json) + ifneq (,$(findstring $(TARGET).json,$(jsons))) + export APP_JSON := $(TOPDIR)/$(TARGET).json + else + ifneq (,$(findstring config.json,$(jsons))) + export APP_JSON := $(TOPDIR)/config.json + endif + endif +else + export APP_JSON := $(TOPDIR)/$(CONFIG_JSON) +endif + +ifeq ($(strip $(ICON)),) + icons := $(wildcard *.jpg) + ifneq (,$(findstring $(TARGET).jpg,$(icons))) + export APP_ICON := $(TOPDIR)/$(TARGET).jpg + else + ifneq (,$(findstring icon.jpg,$(icons))) + export APP_ICON := $(TOPDIR)/icon.jpg + endif + endif +else + export APP_ICON := $(TOPDIR)/$(ICON) +endif + +ifeq ($(strip $(NO_ICON)),) + export NROFLAGS += --icon=$(APP_ICON) +endif + +ifeq ($(strip $(NO_NACP)),) + export NROFLAGS += --nacp=$(CURDIR)/$(TARGET).nacp +endif + +ifneq ($(APP_TITLEID),) + export NACPFLAGS += --titleid=$(APP_TITLEID) +endif + +ifneq ($(ROMFS),) + export NROFLAGS += --romfsdir=$(CURDIR)/$(ROMFS) +endif + +.PHONY: $(TOPTARGETS) $(SUBFOLDERS) all clean + +#--------------------------------------------------------------------------------- +all: $(ROMFS_TARGETS) | $(BUILD) + @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile + +$(BUILD): + @mkdir -p $@ + +ifneq ($(strip $(ROMFS_TARGETS)),) + +$(ROMFS_TARGETS): | $(ROMFS_FOLDERS) + +$(ROMFS_FOLDERS): + @mkdir -p $@ + +$(ROMFS_SHADERS)/%_vsh.dksh: %_vsh.glsl + @echo {vert} $(notdir $<) + @uam -s vert -o $@ $< + +$(ROMFS_SHADERS)/%_tcsh.dksh: %_tcsh.glsl + @echo {tess_ctrl} $(notdir $<) + @uam -s tess_ctrl -o $@ $< + +$(ROMFS_SHADERS)/%_tesh.dksh: %_tesh.glsl + @echo {tess_eval} $(notdir $<) + @uam -s tess_eval -o $@ $< + +$(ROMFS_SHADERS)/%_gsh.dksh: %_gsh.glsl + @echo {geom} $(notdir $<) + @uam -s geom -o $@ $< + +$(ROMFS_SHADERS)/%_fsh.dksh: %_fsh.glsl + @echo {frag} $(notdir $<) + @uam -s frag -o $@ $< + +$(ROMFS_SHADERS)/%.dksh: %.glsl + @echo {comp} $(notdir $<) + @uam -s comp -o $@ $< + +endif + +#--------------------------------------------------------------------------------- +clean: + @echo clean ... +ifeq ($(strip $(APP_JSON)),) + @rm -fr $(BUILD) $(ROMFS_FOLDERS) $(TARGET).nro $(TARGET).nacp $(TARGET).elf +else + @rm -fr $(BUILD) $(ROMFS_FOLDERS) $(TARGET).nsp $(TARGET).nso $(TARGET).npdm $(TARGET).elf +endif + + +#--------------------------------------------------------------------------------- +else +.PHONY: all + +DEPENDS := $(OFILES:.o=.d) + +#--------------------------------------------------------------------------------- +# main targets +#--------------------------------------------------------------------------------- +ifeq ($(strip $(APP_JSON)),) + +all : $(OUTPUT).nro + +ifeq ($(strip $(NO_NACP)),) +$(OUTPUT).nro : $(OUTPUT).elf $(OUTPUT).nacp $(ROMFS_DEPS) +else +$(OUTPUT).nro : $(OUTPUT).elf $(ROMFS_DEPS) +endif + +else + +all : $(OUTPUT).nsp + +$(OUTPUT).nsp : $(OUTPUT).nso $(OUTPUT).npdm + +$(OUTPUT).nso : $(OUTPUT).elf + +endif + +$(OUTPUT).elf : $(OFILES) + +$(OFILES_SRC) : $(HFILES_BIN) + +#--------------------------------------------------------------------------------- +# you need a rule like this for each extension you use as binary data +#--------------------------------------------------------------------------------- +%.bin.o %_bin.h : %.bin +#--------------------------------------------------------------------------------- + @echo $(notdir $<) + @$(bin2o) + +-include $(DEPENDS) + +#--------------------------------------------------------------------------------------- +endif +#--------------------------------------------------------------------------------------- \ No newline at end of file diff --git a/troposphere/daybreak/icon.jpg b/troposphere/daybreak/icon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..867e5d53b6e6b425d7de15d99fceff20b01d9ec8 GIT binary patch literal 22123 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 NfUXo00L=ey0sy>Cuyg. + */ +#include +#include +#include "ams_su.h" +#include "service_guard.h" + +static Service g_amssuSrv; +static TransferMemory g_tmem; + +NX_GENERATE_SERVICE_GUARD(amssu); + +Result _amssuInitialize(void) { + return smGetService(&g_amssuSrv, "ams:su"); +} + +void _amssuCleanup(void) { + serviceClose(&g_amssuSrv); + tmemClose(&g_tmem); +} + +Service *amssuGetServiceSession(void) { + return &g_amssuSrv; +} + +Result amssuGetUpdateInformation(AmsSuUpdateInformation *out, const char *path) { + char send_path[FS_MAX_PATH] = {0}; + strncpy(send_path, path, FS_MAX_PATH-1); + send_path[FS_MAX_PATH-1] = 0; + + return serviceDispatchOut(&g_amssuSrv, 0, *out, + .buffer_attrs = { SfBufferAttr_In | SfBufferAttr_HipcPointer | SfBufferAttr_FixedSize }, + .buffers = { { send_path, FS_MAX_PATH } }, + ); +} + +Result amssuValidateUpdate(AmsSuUpdateValidationInfo *out, const char *path) { + char send_path[FS_MAX_PATH] = {0}; + strncpy(send_path, path, FS_MAX_PATH-1); + send_path[FS_MAX_PATH-1] = 0; + + return serviceDispatchOut(&g_amssuSrv, 1, *out, + .buffer_attrs = { SfBufferAttr_In | SfBufferAttr_HipcPointer | SfBufferAttr_FixedSize }, + .buffers = { { send_path, FS_MAX_PATH } }, + ); +} + +Result amssuSetupUpdate(void *buffer, size_t size, const char *path, bool exfat) { + Result rc = 0; + + if (buffer == NULL) { + rc = tmemCreate(&g_tmem, size, Perm_None); + } else { + rc = tmemCreateFromMemory(&g_tmem, buffer, size, Perm_None); + } + if (R_FAILED(rc)) return rc; + + char send_path[FS_MAX_PATH] = {0}; + strncpy(send_path, path, FS_MAX_PATH-1); + send_path[FS_MAX_PATH-1] = 0; + + const struct { + u8 exfat; + u64 size; + } in = { exfat, g_tmem.size }; + + rc = serviceDispatchIn(&g_amssuSrv, 2, in, + .in_num_handles = 1, + .in_handles = { g_tmem.handle }, + .buffer_attrs = { SfBufferAttr_In | SfBufferAttr_HipcPointer | SfBufferAttr_FixedSize }, + .buffers = { { send_path, FS_MAX_PATH } }, + ); + if (R_FAILED((rc))) { + tmemClose(&g_tmem); + } + + return rc; +} + +Result amssuSetupUpdateWithVariation(void *buffer, size_t size, const char *path, bool exfat, u32 variation) { + Result rc = 0; + + if (buffer == NULL) { + rc = tmemCreate(&g_tmem, size, Perm_None); + } else { + rc = tmemCreateFromMemory(&g_tmem, buffer, size, Perm_None); + } + if (R_FAILED(rc)) return rc; + + char send_path[FS_MAX_PATH] = {0}; + strncpy(send_path, path, FS_MAX_PATH-1); + send_path[FS_MAX_PATH-1] = 0; + + const struct { + u8 exfat; + u32 variation; + u64 size; + } in = { exfat, variation, g_tmem.size }; + + rc = serviceDispatchIn(&g_amssuSrv, 3, in, + .in_num_handles = 1, + .in_handles = { g_tmem.handle }, + .buffer_attrs = { SfBufferAttr_In | SfBufferAttr_HipcPointer | SfBufferAttr_FixedSize }, + .buffers = { { send_path, FS_MAX_PATH } }, + ); + if (R_FAILED((rc))) { + tmemClose(&g_tmem); + } + + return rc; +} + +Result amssuRequestPrepareUpdate(AsyncResult *a) { + memset(a, 0, sizeof(*a)); + + Handle event = INVALID_HANDLE; + Result rc = serviceDispatch(&g_amssuSrv, 4, + .out_num_objects = 1, + .out_objects = &a->s, + .out_handle_attrs = { SfOutHandleAttr_HipcCopy }, + .out_handles = &event, + ); + + if (R_SUCCEEDED(rc)) + eventLoadRemote(&a->event, event, false); + + return rc; +} + +Result amssuGetPrepareUpdateProgress(NsSystemUpdateProgress *out) { + return serviceDispatchOut(&g_amssuSrv, 5, *out); +} + +Result amssuHasPreparedUpdate(bool *out) { + u8 outval = 0; + Result rc = serviceDispatchOut(&g_amssuSrv, 6, outval); + if (R_SUCCEEDED(rc)) { + if (out) *out = outval & 1; + } + return rc; +} + +Result amssuApplyPreparedUpdate() { + return serviceDispatch(&g_amssuSrv, 7); +} \ No newline at end of file diff --git a/troposphere/daybreak/source/ams_su.h b/troposphere/daybreak/source/ams_su.h new file mode 100644 index 000000000..d38e183fe --- /dev/null +++ b/troposphere/daybreak/source/ams_su.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2018-2020 Atmosphère-NX + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + u32 version; + bool exfat_supported; + u32 num_firmware_variations; + u32 firmware_variation_ids[16]; +} AmsSuUpdateInformation; + +typedef struct { + Result result; + NcmContentMetaKey invalid_key; + NcmContentId invalid_content_id; +} AmsSuUpdateValidationInfo; + +Result amssuInitialize(); +void amssuExit(); +Service *amssuGetServiceSession(void); + +Result amssuGetUpdateInformation(AmsSuUpdateInformation *out, const char *path); +Result amssuValidateUpdate(AmsSuUpdateValidationInfo *out, const char *path); +Result amssuSetupUpdate(void *buffer, size_t size, const char *path, bool exfat); +Result amssuSetupUpdateWithVariation(void *buffer, size_t size, const char *path, bool exfat, u32 variation); +Result amssuRequestPrepareUpdate(AsyncResult *a); +Result amssuGetPrepareUpdateProgress(NsSystemUpdateProgress *out); +Result amssuHasPreparedUpdate(bool *out); +Result amssuApplyPreparedUpdate(); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/troposphere/daybreak/source/assert.hpp b/troposphere/daybreak/source/assert.hpp new file mode 100644 index 000000000..1b8ed4d5f --- /dev/null +++ b/troposphere/daybreak/source/assert.hpp @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 Adubbz + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include + +#define DBK_ABORT_UNLESS(expr) \ + if (!static_cast(expr)) { \ + std::abort(); \ + } diff --git a/troposphere/daybreak/source/main.cpp b/troposphere/daybreak/source/main.cpp new file mode 100644 index 000000000..ef95b363f --- /dev/null +++ b/troposphere/daybreak/source/main.cpp @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2020 Adubbz + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include +#include +#include +#include +#include +#include "ui.hpp" +#include "ams_su.h" + +extern "C" { + + void userAppInit(void) { + Result rc = 0; + + if (R_FAILED(rc = amssuInitialize())) { + fatalThrow(rc); + } + + if (R_FAILED(rc = romfsInit())) { + fatalThrow(rc); + } + + if (R_FAILED(rc = spsmInitialize())) { + fatalThrow(rc); + } + + if (R_FAILED(rc = plInitialize(PlServiceType_User))) { + fatalThrow(rc); + } + } + + void userAppExit(void) { + romfsExit(); + plExit(); + spsmExit(); + amssuExit(); + } + +} + +namespace { + + static constexpr u32 FramebufferWidth = 1280; + static constexpr u32 FramebufferHeight = 720; + +} + +class Daybreak : public CApplication { + private: + static constexpr unsigned NumFramebuffers = 2; + static constexpr unsigned StaticCmdSize = 0x1000; + + dk::UniqueDevice m_device; + dk::UniqueQueue m_queue; + dk::UniqueSwapchain m_swapchain; + + std::optional m_pool_images; + std::optional m_pool_code; + std::optional m_pool_data; + + dk::UniqueCmdBuf m_cmd_buf; + DkCmdList m_render_cmdlist; + + dk::Image m_depth_buffer; + CMemPool::Handle m_depth_buffer_mem; + dk::Image m_framebuffers[NumFramebuffers]; + CMemPool::Handle m_framebuffers_mem[NumFramebuffers]; + DkCmdList m_framebuffer_cmdlists[NumFramebuffers]; + + std::optional m_renderer; + NVGcontext *m_vg; + int m_standard_font; + public: + Daybreak() { + Result rc = 0; + + /* Create the deko3d device. */ + m_device = dk::DeviceMaker{}.create(); + + /* Create the main queue. */ + m_queue = dk::QueueMaker{m_device}.setFlags(DkQueueFlags_Graphics).create(); + + /* Create the memory pools. */ + m_pool_images.emplace(m_device, DkMemBlockFlags_GpuCached | DkMemBlockFlags_Image, 16*1024*1024); + m_pool_code.emplace(m_device, DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached | DkMemBlockFlags_Code, 128*1024); + m_pool_data.emplace(m_device, DkMemBlockFlags_CpuUncached | DkMemBlockFlags_GpuCached, 1*1024*1024); + + /* Create the static command buffer and feed it freshly allocated memory. */ + m_cmd_buf = dk::CmdBufMaker{m_device}.create(); + CMemPool::Handle cmdmem = m_pool_data->allocate(StaticCmdSize); + m_cmd_buf.addMemory(cmdmem.getMemBlock(), cmdmem.getOffset(), cmdmem.getSize()); + + /* Create the framebuffer resources. */ + this->CreateFramebufferResources(); + + m_renderer.emplace(FramebufferWidth, FramebufferHeight, m_device, m_queue, *m_pool_images, *m_pool_code, *m_pool_data); + m_vg = nvgCreateDk(&*m_renderer, NVG_ANTIALIAS | NVG_STENCIL_STROKES); + + + PlFontData font; + if (R_FAILED(rc = plGetSharedFontByType(&font, PlSharedFontType_Standard))) { + fatalThrow(rc); + } + + m_standard_font = nvgCreateFontMem(m_vg, "switch-standard", static_cast(font.address), font.size, 0); + } + + ~Daybreak() { + /* Destroy the framebuffer resources. This should be done first. */ + this->DestroyFramebufferResources(); + + /* Cleanup vg. */ + nvgDeleteDk(m_vg); + + /* Destroy the renderer. */ + m_renderer.reset(); + } + private: + void CreateFramebufferResources() { + /* Create layout for the depth buffer. */ + dk::ImageLayout layout_depth_buffer; + dk::ImageLayoutMaker{m_device} + .setFlags(DkImageFlags_UsageRender | DkImageFlags_HwCompression) + .setFormat(DkImageFormat_S8) + .setDimensions(FramebufferWidth, FramebufferHeight) + .initialize(layout_depth_buffer); + + /* Create the depth buffer. */ + m_depth_buffer_mem = m_pool_images->allocate(layout_depth_buffer.getSize(), layout_depth_buffer.getAlignment()); + m_depth_buffer.initialize(layout_depth_buffer, m_depth_buffer_mem.getMemBlock(), m_depth_buffer_mem.getOffset()); + + /* Create layout for the framebuffers. */ + dk::ImageLayout layout_framebuffer; + dk::ImageLayoutMaker{m_device} + .setFlags(DkImageFlags_UsageRender | DkImageFlags_UsagePresent | DkImageFlags_HwCompression) + .setFormat(DkImageFormat_RGBA8_Unorm) + .setDimensions(FramebufferWidth, FramebufferHeight) + .initialize(layout_framebuffer); + + /* Create the framebuffers. */ + std::array fb_array; + const u64 fb_size = layout_framebuffer.getSize(); + const u32 fb_align = layout_framebuffer.getAlignment(); + + for (unsigned int i = 0; i < NumFramebuffers; i++) { + /* Allocate a framebuffer. */ + m_framebuffers_mem[i] = m_pool_images->allocate(fb_size, fb_align); + m_framebuffers[i].initialize(layout_framebuffer, m_framebuffers_mem[i].getMemBlock(), m_framebuffers_mem[i].getOffset()); + + /* Generate a command list that binds it. */ + dk::ImageView color_target{ m_framebuffers[i] }, depth_target{ m_depth_buffer }; + m_cmd_buf.bindRenderTargets(&color_target, &depth_target); + m_framebuffer_cmdlists[i] = m_cmd_buf.finishList(); + + /* Fill in the array for use later by the swapchain creation code. */ + fb_array[i] = &m_framebuffers[i]; + } + + /* Create the swapchain using the framebuffers. */ + m_swapchain = dk::SwapchainMaker{m_device, nwindowGetDefault(), fb_array}.create(); + + /* Generate the main rendering cmdlist. */ + this->RecordStaticCommands(); + } + + void DestroyFramebufferResources() { + /* Return early if we have nothing to destroy. */ + if (!m_swapchain) return; + + /* Make sure the queue is idle before destroying anything. */ + m_queue.waitIdle(); + + /* Clear the static cmdbuf, destroying the static cmdlists in the process. */ + m_cmd_buf.clear(); + + /* Destroy the swapchain. */ + m_swapchain.destroy(); + + /* Destroy the framebuffers. */ + for (unsigned int i = 0; i < NumFramebuffers; i ++) { + m_framebuffers_mem[i].destroy(); + } + + /* Destroy the depth buffer. */ + m_depth_buffer_mem.destroy(); + } + + void RecordStaticCommands() { + /* Initialize state structs with deko3d defaults. */ + dk::RasterizerState rasterizer_state; + dk::ColorState color_state; + dk::ColorWriteState color_write_state; + + /* Configure the viewport and scissor. */ + m_cmd_buf.setViewports(0, { { 0.0f, 0.0f, FramebufferWidth, FramebufferHeight, 0.0f, 1.0f } }); + m_cmd_buf.setScissors(0, { { 0, 0, FramebufferWidth, FramebufferHeight } }); + + /* Clear the color and depth buffers. */ + m_cmd_buf.clearColor(0, DkColorMask_RGBA, 0.f, 0.f, 0.f, 1.0f); + m_cmd_buf.clearDepthStencil(true, 1.0f, 0xFF, 0); + + /* Bind required state. */ + m_cmd_buf.bindRasterizerState(rasterizer_state); + m_cmd_buf.bindColorState(color_state); + m_cmd_buf.bindColorWriteState(color_write_state); + + m_render_cmdlist = m_cmd_buf.finishList(); + } + + void Render(u64 ns) { + /* Acquire a framebuffer from the swapchain (and wait for it to be available). */ + int slot = m_queue.acquireImage(m_swapchain); + + /* Run the command list that attaches said framebuffer to the queue. */ + m_queue.submitCommands(m_framebuffer_cmdlists[slot]); + + /* Run the main rendering command list. */ + m_queue.submitCommands(m_render_cmdlist); + + nvgBeginFrame(m_vg, FramebufferWidth, FramebufferHeight, 1.0f); + dbk::RenderMenu(m_vg, ns); + nvgEndFrame(m_vg); + + /* Now that we are done rendering, present it to the screen. */ + m_queue.presentImage(m_swapchain, slot); + } + + public: + bool onFrame(u64 ns) override { + dbk::UpdateMenu(ns); + this->Render(ns); + return !dbk::IsExitRequested(); + } +}; + +int main(int argc, char **argv) { + /* Initialize the menu. */ + dbk::InitializeMenu(FramebufferWidth, FramebufferHeight); + + Daybreak daybreak; + daybreak.run(); + return 0; +} diff --git a/troposphere/daybreak/source/service_guard.h b/troposphere/daybreak/source/service_guard.h new file mode 100644 index 000000000..5fbc5fca9 --- /dev/null +++ b/troposphere/daybreak/source/service_guard.h @@ -0,0 +1,56 @@ +#pragma once +#include +#include +#include +#include +#include + +typedef struct ServiceGuard { + Mutex mutex; + u32 refCount; +} ServiceGuard; + +NX_INLINE bool serviceGuardBeginInit(ServiceGuard* g) +{ + mutexLock(&g->mutex); + return (g->refCount++) == 0; +} + +NX_INLINE Result serviceGuardEndInit(ServiceGuard* g, Result rc, void (*cleanupFunc)(void)) +{ + if (R_FAILED(rc)) { + cleanupFunc(); + --g->refCount; + } + mutexUnlock(&g->mutex); + return rc; +} + +NX_INLINE void serviceGuardExit(ServiceGuard* g, void (*cleanupFunc)(void)) +{ + mutexLock(&g->mutex); + if (g->refCount && (--g->refCount) == 0) + cleanupFunc(); + mutexUnlock(&g->mutex); +} + +#define NX_GENERATE_SERVICE_GUARD_PARAMS(name, _paramdecl, _parampass) \ +\ +static ServiceGuard g_##name##Guard; \ +NX_INLINE Result _##name##Initialize _paramdecl; \ +static void _##name##Cleanup(void); \ +\ +Result name##Initialize _paramdecl \ +{ \ + Result rc = 0; \ + if (serviceGuardBeginInit(&g_##name##Guard)) \ + rc = _##name##Initialize _parampass; \ + return serviceGuardEndInit(&g_##name##Guard, rc, _##name##Cleanup); \ +} \ +\ +void name##Exit(void) \ +{ \ + serviceGuardExit(&g_##name##Guard, _##name##Cleanup); \ +} + +#define NX_GENERATE_SERVICE_GUARD(name) NX_GENERATE_SERVICE_GUARD_PARAMS(name, (void), ()) \ No newline at end of file diff --git a/troposphere/daybreak/source/ui.cpp b/troposphere/daybreak/source/ui.cpp new file mode 100644 index 000000000..4bae0d5f4 --- /dev/null +++ b/troposphere/daybreak/source/ui.cpp @@ -0,0 +1,966 @@ +/* + * Copyright (c) 2020 Adubbz + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include +#include +#include +#include +#include "ui.hpp" +#include "ui_util.hpp" +#include "assert.hpp" + +namespace dbk { + + namespace { + + u32 g_screen_width; + u32 g_screen_height; + + std::shared_ptr g_current_menu; + bool g_initialized = false; + bool g_exit_requested = false; + + u32 g_prev_touch_count = -1; + touchPosition g_start_touch_position; + bool g_started_touching = false; + bool g_tapping = false; + bool g_touches_moving = false; + bool g_finished_touching = false; + + /* Update install state. */ + char g_update_path[FS_MAX_PATH]; + bool g_use_exfat = false; + + constexpr u32 MaxTapMovement = 20; + + void UpdateInput() { + /* Update the previous touch count. */ + g_prev_touch_count = hidTouchCount(); + + /* Scan for input and update touch state. */ + hidScanInput(); + const u32 touch_count = hidTouchCount(); + + if (g_prev_touch_count == 0 && touch_count > 0) { + hidTouchRead(&g_start_touch_position, 0); + g_started_touching = true; + g_tapping = true; + } else { + g_started_touching = false; + } + + if (g_prev_touch_count > 0 && touch_count == 0) { + g_finished_touching = true; + g_tapping = false; + } else { + g_finished_touching = false; + } + + /* Check if currently moving. */ + if (g_prev_touch_count > 0 && touch_count > 0) { + touchPosition current_touch_position; + hidTouchRead(¤t_touch_position, 0); + + if ((abs(current_touch_position.px - g_start_touch_position.px) > MaxTapMovement || abs(current_touch_position.py - g_start_touch_position.py) > MaxTapMovement)) { + g_touches_moving = true; + g_tapping = false; + } else { + g_touches_moving = false; + } + } else { + g_touches_moving = false; + } + } + + void ChangeMenu(std::shared_ptr menu) { + g_current_menu = menu; + } + + void ReturnToPreviousMenu() { + /* Go to the previous menu if there is one. */ + if (g_current_menu->GetPrevMenu() != nullptr) { + g_current_menu = g_current_menu->GetPrevMenu(); + } + } + + Result IsPathBottomLevel(const char *path, bool *out) { + Result rc = 0; + FsFileSystem *fs; + char translated_path[FS_MAX_PATH] = {}; + DBK_ABORT_UNLESS(fsdevTranslatePath(path, &fs, translated_path) != -1); + + FsDir dir; + if (R_FAILED(rc = fsFsOpenDirectory(fs, translated_path, FsDirOpenMode_ReadDirs, &dir))) { + return rc; + } + + s64 entry_count; + if (R_FAILED(rc = fsDirGetEntryCount(&dir, &entry_count))) { + return rc; + } + + *out = entry_count == 0; + fsDirClose(&dir); + return rc; + } + + } + + void Menu::AddButton(u32 id, const char *text, float x, float y, float w, float h) { + DBK_ABORT_UNLESS(id < MaxButtons); + Button button = { + .id = id, + .selected = false, + .enabled = true, + .x = x, + .y = y, + .w = w, + .h = h, + }; + + strncpy(button.text, text, sizeof(button.text)-1); + m_buttons[id] = button; + } + + void Menu::SetButtonSelected(u32 id, bool selected) { + DBK_ABORT_UNLESS(id < MaxButtons); + auto &button = m_buttons[id]; + + if (button) { + button->selected = selected; + } + } + + void Menu::DeselectAllButtons() { + for (auto &button : m_buttons) { + /* Ensure button is present. */ + if (!button) { + continue; + } + button->selected = false; + } + } + + void Menu::SetButtonEnabled(u32 id, bool enabled) { + DBK_ABORT_UNLESS(id < MaxButtons); + auto &button = m_buttons[id]; + button->enabled = enabled; + } + + Button *Menu::GetButton(u32 id) { + DBK_ABORT_UNLESS(id < MaxButtons); + return !m_buttons[id] ? nullptr : &(*m_buttons[id]); + } + + Button *Menu::GetSelectedButton() { + for (auto &button : m_buttons) { + if (button && button->enabled && button->selected) { + return &(*button); + } + } + + return nullptr; + } + + Button *Menu::GetClosestButtonToSelection(Direction direction) { + const Button *selected_button = this->GetSelectedButton(); + + if (selected_button == nullptr || direction == Direction::Invalid) { + return nullptr; + } + + Button *closest_button = nullptr; + float closest_distance = 0.0f; + + for (auto &button : m_buttons) { + /* Skip absent button. */ + if (!button || !button->enabled) { + continue; + } + + /* Skip buttons that are in the wrong direction. */ + if ((direction == Direction::Down && button->y <= selected_button->y) || + (direction == Direction::Up && button->y >= selected_button->y) || + (direction == Direction::Right && button->x <= selected_button->x) || + (direction == Direction::Left && button->x >= selected_button->x)) { + continue; + } + + const float x_dist = button->x - selected_button->x; + const float y_dist = button->y - selected_button->y; + const float sq_dist = x_dist * x_dist + y_dist * y_dist; + + /* If we don't already have a closest button, set it. */ + if (closest_button == nullptr) { + closest_button = &(*button); + closest_distance = sq_dist; + continue; + } + + /* Update the closest button if this one is closer. */ + if (sq_dist < closest_distance) { + closest_button = &(*button); + closest_distance = sq_dist; + } + } + + return closest_button; + } + + Button *Menu::GetTouchedButton() { + touchPosition touch; + const u32 touch_count = hidTouchCount(); + + for (u32 i = 0; i < touch_count && g_started_touching; i++) { + hidTouchRead(&touch, i); + + for (auto &button : m_buttons) { + if (button && button->enabled && button->IsPositionInBounds(touch.px, touch.py)) { + return &(*button); + } + } + } + + return nullptr; + } + + Button *Menu::GetActivatedButton() { + Button *selected_button = this->GetSelectedButton(); + + if (selected_button == nullptr) { + return nullptr; + } + + const u64 k_down = hidKeysDown(CONTROLLER_P1_AUTO); + + if (k_down & KEY_A || this->GetTouchedButton() == selected_button) { + return selected_button; + } + + return nullptr; + } + + void Menu::UpdateButtons() { + const u64 k_down = hidKeysDown(CONTROLLER_P1_AUTO); + Direction direction = Direction::Invalid; + + if (k_down & KEY_DOWN) { + direction = Direction::Down; + } else if (k_down & KEY_UP) { + direction = Direction::Up; + } else if (k_down & KEY_LEFT) { + direction = Direction::Left; + } else if (k_down & KEY_RIGHT) { + direction = Direction::Right; + } + + /* Select the closest button. */ + if (const Button *closest_button = this->GetClosestButtonToSelection(direction); closest_button != nullptr) { + this->DeselectAllButtons(); + this->SetButtonSelected(closest_button->id, true); + } + + /* Select the touched button. */ + if (const Button *touched_button = this->GetTouchedButton(); touched_button != nullptr) { + this->DeselectAllButtons(); + this->SetButtonSelected(touched_button->id, true); + } + } + + void Menu::DrawButtons(NVGcontext *vg, u64 ns) { + for (auto &button : m_buttons) { + /* Ensure button is present. */ + if (!button) { + continue; + } + + /* Set the button style. */ + auto style = ButtonStyle::StandardDisabled; + if (button->enabled) { + style = button->selected ? ButtonStyle::StandardSelected : ButtonStyle::Standard; + } + + DrawButton(vg, button->text, button->x, button->y, button->w, button->h, style, ns); + } + } + + void Menu::LogText(const char *format, ...) { + /* Create a temporary string. */ + char tmp[0x100]; + va_list args; + va_start(args, format); + vsnprintf(tmp, sizeof(tmp)-1, format, args); + va_end(args); + + /* Append the text to the log buffer. */ + strncat(m_log_buffer, tmp, sizeof(m_log_buffer)-1); + } + + std::shared_ptr Menu::GetPrevMenu() { + return m_prev_menu; + } + + MainMenu::MainMenu() : Menu(nullptr) { + const float x = g_screen_width / 2.0f - WindowWidth / 2.0f; + const float y = g_screen_height / 2.0f - WindowHeight / 2.0f; + + this->AddButton(InstallButtonId, "Install", x + ButtonHorizontalPadding, y + TitleGap, WindowWidth - ButtonHorizontalPadding * 2, ButtonHeight); + this->AddButton(ExitButtonId, "Exit", x + ButtonHorizontalPadding, y + TitleGap + ButtonHeight + ButtonVerticalGap, WindowWidth - ButtonHorizontalPadding * 2, ButtonHeight); + this->SetButtonSelected(InstallButtonId, true); + } + + void MainMenu::Update(u64 ns) { + u64 k_down = hidKeysDown(CONTROLLER_P1_AUTO); + + if (k_down & KEY_B) { + g_exit_requested = true; + } + + /* Take action if a button has been activated. */ + if (const Button *activated_button = this->GetActivatedButton(); activated_button != nullptr) { + switch (activated_button->id) { + case InstallButtonId: + ChangeMenu(std::make_shared(g_current_menu, "/")); + break; + case ExitButtonId: + g_exit_requested = true; + break; + } + } + + this->UpdateButtons(); + + /* Fallback on selecting the install button. */ + if (const Button *selected_button = this->GetSelectedButton(); k_down && selected_button == nullptr) { + this->SetButtonSelected(InstallButtonId, true); + } + } + + void MainMenu::Draw(NVGcontext *vg, u64 ns) { + DrawWindow(vg, "Daybreak", g_screen_width / 2.0f - WindowWidth / 2.0f, g_screen_height / 2.0f - WindowHeight / 2.0f, WindowWidth, WindowHeight); + this->DrawButtons(vg, ns); + } + + FileMenu::FileMenu(std::shared_ptr prev_menu, const char *root) : Menu(prev_menu), m_current_index(0), m_scroll_offset(0), m_touch_start_scroll_offset(0), m_touch_finalize_selection(false) { + Result rc = 0; + + strncpy(m_root, root, sizeof(m_root)-1); + + if (R_FAILED(rc = this->PopulateFileEntries())) { + fatalThrow(rc); + } + } + + Result FileMenu::PopulateFileEntries() { + /* Open the directory. */ + DIR *dir = opendir(m_root); + if (dir == nullptr) { + return fsdevGetLastResult(); + } + + /* Add file entries to the list. */ + struct dirent *ent; + while ((ent = readdir(dir)) != nullptr) { + if (ent->d_type == DT_DIR) { + FileEntry file_entry = {}; + strncpy(file_entry.name, ent->d_name, sizeof(file_entry.name)); + m_file_entries.push_back(file_entry); + } + } + + /* Close the directory. */ + closedir(dir); + return 0; + } + + bool FileMenu::IsSelectionVisible() { + const float visible_start = m_scroll_offset; + const float visible_end = visible_start + FileListHeight; + const float entry_start = static_cast(m_current_index) * (FileRowHeight + FileRowGap); + const float entry_end = entry_start + (FileRowHeight + FileRowGap); + return entry_start >= visible_start && entry_end <= visible_end; + } + + void FileMenu::ScrollToSelection() { + const float visible_start = m_scroll_offset; + const float visible_end = visible_start + FileListHeight; + const float entry_start = static_cast(m_current_index) * (FileRowHeight + FileRowGap); + const float entry_end = entry_start + (FileRowHeight + FileRowGap); + + if (entry_end > visible_end) { + m_scroll_offset += entry_end - visible_end; + } else if (entry_end < visible_end) { + m_scroll_offset = entry_start; + } + } + + bool FileMenu::IsEntryTouched(u32 i) { + const float x = g_screen_width / 2.0f - WindowWidth / 2.0f; + const float y = g_screen_height / 2.0f - WindowHeight / 2.0f; + + touchPosition current_pos; + hidTouchRead(¤t_pos, 0); + + /* Check if the tap is within the x bounds. */ + if (current_pos.px >= x + TextBackgroundOffset + FileRowHorizontalInset && current_pos.px <= WindowWidth - (TextBackgroundOffset + FileRowHorizontalInset) * 2.0f) { + const float y_min = y + TitleGap + FileRowGap + i * (FileRowHeight + FileRowGap) - m_scroll_offset; + const float y_max = y_min + FileRowHeight; + + /* Check if the tap is within the y bounds. */ + if (current_pos.py >= y_min && current_pos.py <= y_max) { + return true; + } + } + + return false; + } + + void FileMenu::UpdateTouches() { + /* Setup values on initial touch. */ + if (g_started_touching) { + m_touch_start_scroll_offset = m_scroll_offset; + + /* We may potentially finalize the selection later if we start off touching it. */ + if (this->IsEntryTouched(m_current_index)) { + m_touch_finalize_selection = true; + } + } + + /* Scroll based on touch movement. */ + if (g_touches_moving) { + touchPosition current_pos; + hidTouchRead(¤t_pos, 0); + + const int dist_y = current_pos.py - g_start_touch_position.py; + float new_scroll_offset = m_touch_start_scroll_offset - static_cast(dist_y); + float max_scroll = (FileRowHeight + FileRowGap) * static_cast(m_file_entries.size()) - FileListHeight; + + /* Don't allow scrolling if there is not enough elements. */ + if (max_scroll < 0.0f) { + max_scroll = 0.0f; + } + + /* Don't allow scrolling before the first element. */ + if (new_scroll_offset < 0.0f) { + new_scroll_offset = 0.0f; + } + + /* Don't allow scrolling past the last element. */ + if (new_scroll_offset > max_scroll) { + new_scroll_offset = max_scroll; + } + + m_scroll_offset = new_scroll_offset; + } + + /* Select any tapped entries. */ + if (g_tapping) { + for (u32 i = 0; i < m_file_entries.size(); i++) { + if (this->IsEntryTouched(i)) { + /* The current index is checked later. */ + if (i == m_current_index) { + continue; + } + + m_current_index = i; + + /* Don't finalize selection if we touch something else. */ + m_touch_finalize_selection = false; + break; + } + } + } + + /* Don't finalize selection if we aren't finished and we've either stopped tapping or are no longer touching the selection. */ + if (!g_finished_touching && (!g_tapping || !this->IsEntryTouched(m_current_index))) { + m_touch_finalize_selection = false; + } + + /* Finalize selection if the currently selected entry is touched for the second time. */ + if (g_finished_touching && m_touch_finalize_selection) { + this->FinalizeSelection(); + m_touch_finalize_selection = false; + } + } + + void FileMenu::FinalizeSelection() { + DBK_ABORT_UNLESS(m_current_index < m_file_entries.size()); + FileEntry &entry = m_file_entries[m_current_index]; + + /* Determine the selected path. */ + char current_path[FS_MAX_PATH] = {}; + snprintf(current_path, sizeof(current_path)-1, "%s%s", m_root, entry.name); + + /* Determine if the chosen path is the bottom level. */ + Result rc = 0; + bool bottom_level; + if (R_FAILED(rc = IsPathBottomLevel(current_path, &bottom_level))) { + fatalThrow(rc); + } + + /* Show exfat settings or the next file menu. */ + if (bottom_level) { + /* Set the update path. */ + snprintf(g_update_path, sizeof(g_update_path)-1, "%s", current_path); + + /* Change the menu. */ + ChangeMenu(std::make_shared(g_current_menu)); + } else { + ChangeMenu(std::make_shared(g_current_menu, current_path)); + } + } + + void FileMenu::Update(u64 ns) { + u64 k_down = hidKeysDown(CONTROLLER_P1_AUTO); + + /* Go back if B is pressed. */ + if (k_down & KEY_B) { + ReturnToPreviousMenu(); + return; + } + + /* Finalize selection on pressing A. */ + if (k_down & KEY_A) { + this->FinalizeSelection(); + } + + /* Update touch input. */ + this->UpdateTouches(); + + const u32 prev_index = m_current_index; + + if (k_down & KEY_DOWN) { + /* Scroll down. */ + if (m_current_index >= (m_file_entries.size() - 1)) { + m_current_index = 0; + } else { + m_current_index++; + } + } else if (k_down & KEY_UP) { + /* Scroll up. */ + if (m_current_index == 0) { + m_current_index = m_file_entries.size() - 1; + } else { + m_current_index--; + } + } + + /* Scroll to the selection if it isn't visible. */ + if (prev_index != m_current_index && !this->IsSelectionVisible()) { + this->ScrollToSelection(); + } + } + + void FileMenu::Draw(NVGcontext *vg, u64 ns) { + const float x = g_screen_width / 2.0f - WindowWidth / 2.0f; + const float y = g_screen_height / 2.0f - WindowHeight / 2.0f; + + DrawWindow(vg, "Select an update directory", x, y, WindowWidth, WindowHeight); + DrawTextBackground(vg, x + TextBackgroundOffset, y + TitleGap, WindowWidth - TextBackgroundOffset * 2.0f, (FileRowHeight + FileRowGap) * MaxFileRows + FileRowGap); + + nvgSave(vg); + nvgScissor(vg, x + TextBackgroundOffset, y + TitleGap, WindowWidth - TextBackgroundOffset * 2.0f, (FileRowHeight + FileRowGap) * MaxFileRows + FileRowGap); + + for (u32 i = 0; i < m_file_entries.size(); i++) { + FileEntry &entry = m_file_entries[i]; + auto style = ButtonStyle::FileSelect; + + if (i == m_current_index) { + style = ButtonStyle::FileSelectSelected; + } + + DrawButton(vg, entry.name, x + TextBackgroundOffset + FileRowHorizontalInset, y + TitleGap + FileRowGap + i * (FileRowHeight + FileRowGap) - m_scroll_offset, WindowWidth - (TextBackgroundOffset + FileRowHorizontalInset) * 2.0f, FileRowHeight, style, ns); + } + + nvgRestore(vg); + } + + ValidateUpdateMenu::ValidateUpdateMenu(std::shared_ptr prev_menu) : Menu(prev_menu), m_has_drawn(false), m_has_info(false), m_has_validated(false) { + const float x = g_screen_width / 2.0f - WindowWidth / 2.0f; + const float y = g_screen_height / 2.0f - WindowHeight / 2.0f; + + /* Add buttons. */ + this->AddButton(BackButtonId, "Back", x + HorizontalGap, y + WindowHeight - BottomGap - ButtonHeight, ButtonWidth, ButtonHeight); + this->AddButton(ContinueButtonId, "Continue", x + HorizontalGap + ButtonWidth + ButtonHorizontalGap, y + WindowHeight - BottomGap - ButtonHeight, ButtonWidth, ButtonHeight); + this->SetButtonEnabled(BackButtonId, false); + this->SetButtonEnabled(ContinueButtonId, false); + + /* Obtain update information. */ + if (R_FAILED(this->GetUpdateInformation())) { + this->SetButtonEnabled(BackButtonId, true); + this->SetButtonSelected(BackButtonId, true); + } else { + /* Log this early so it is printed out before validation causes stalling. */ + this->LogText("Validating update, this may take a moment...\n"); + } + } + + Result ValidateUpdateMenu::GetUpdateInformation() { + Result rc = 0; + this->LogText("Directory %s\n", g_update_path); + + /* Attempt to get the update information. */ + if (R_FAILED(rc = amssuGetUpdateInformation(&m_update_info, g_update_path))) { + if (rc == 0x1a405) { + this->LogText("No update found in folder.\nEnsure your ncas are named correctly!\nResult: 0x%08x\n", rc); + } else { + this->LogText("Failed to get update information.\nResult: 0x%08x\n", rc); + } + return rc; + } + + /* Print update information. */ + this->LogText("- Version: %d.%d.%d\n", (m_update_info.version >> 26) & 0x1f, (m_update_info.version >> 20) & 0x1f, (m_update_info.version >> 16) & 0xf); + if (m_update_info.exfat_supported) { + this->LogText("- exFAT: Supported\n"); + } else { + this->LogText("- exFAT: Unsupported\n"); + } + this->LogText("- Firmware variations: %d\n", m_update_info.num_firmware_variations); + + /* Mark as having obtained update info. */ + m_has_info = true; + return rc; + } + + void ValidateUpdateMenu::ValidateUpdate() { + Result rc = 0; + + /* Validate the update. */ + if (R_FAILED(rc = amssuValidateUpdate(&m_validation_info, g_update_path))) { + this->LogText("Failed to validate update.\nResult: 0x%08x\n", rc); + return; + } + + /* Check the result. */ + if (R_SUCCEEDED(m_validation_info.result)) { + this->LogText("Update is valid!\n"); + + /* Enable the back and continue buttons and select the continue button. */ + this->SetButtonEnabled(BackButtonId, true); + this->SetButtonEnabled(ContinueButtonId, true); + this->SetButtonSelected(ContinueButtonId, true); + } else { + /* Log the missing content info. */ + const u32 version = m_validation_info.invalid_key.version; + this->LogText("Validation failed with result: 0x%08x\n", m_validation_info.result); + this->LogText("Missing content:\n- Program id: %016lx\n- Version: %d.%d.%d\n", m_validation_info.invalid_key.id, (version >> 26) & 0x1f, (version >> 20) & 0x1f, (version >> 16) & 0xf); + + /* Log the missing content id. */ + this->LogText("- Content id: "); + for (size_t i = 0; i < sizeof(NcmContentId); i++) { + this->LogText("%02x", m_validation_info.invalid_content_id.c[i]); + } + this->LogText("\n"); + + /* Enable the back button and select it. */ + this->SetButtonEnabled(BackButtonId, true); + this->SetButtonSelected(BackButtonId, true); + } + + /* Mark validation as being complete. */ + m_has_validated = true; + } + + void ValidateUpdateMenu::Update(u64 ns) { + /* Perform validation if it hasn't been done already. */ + if (m_has_info && m_has_drawn && !m_has_validated) { + this->ValidateUpdate(); + } + + u64 k_down = hidKeysDown(CONTROLLER_P1_AUTO); + + /* Go back if B is pressed. */ + if (k_down & KEY_B) { + ReturnToPreviousMenu(); + return; + } + + /* Take action if a button has been activated. */ + if (const Button *activated_button = this->GetActivatedButton(); activated_button != nullptr) { + switch (activated_button->id) { + case BackButtonId: + ReturnToPreviousMenu(); + return; + case ContinueButtonId: + /* Don't continue if validation hasn't been done or has failed. */ + if (!m_has_validated || R_FAILED(m_validation_info.result)) { + break; + } + + if (m_update_info.exfat_supported) { + ChangeMenu(std::make_shared(g_current_menu)); + } else { + g_use_exfat = false; + ChangeMenu(std::make_shared(g_current_menu)); + } + + return; + } + } + + this->UpdateButtons(); + } + + void ValidateUpdateMenu::Draw(NVGcontext *vg, u64 ns) { + const float x = g_screen_width / 2.0f - WindowWidth / 2.0f; + const float y = g_screen_height / 2.0f - WindowHeight / 2.0f; + + DrawWindow(vg, "Update information", x, y, WindowWidth, WindowHeight); + DrawTextBackground(vg, x + HorizontalGap, y + TitleGap, WindowWidth - HorizontalGap * 2.0f, TextAreaHeight); + DrawTextBlock(vg, m_log_buffer, x + HorizontalGap + TextHorizontalInset, y + TitleGap + TextVerticalInset, WindowWidth - (HorizontalGap + TextHorizontalInset) * 2.0f, TextAreaHeight - TextVerticalInset * 2.0f); + + this->DrawButtons(vg, ns); + m_has_drawn = true; + } + + ChooseExfatMenu::ChooseExfatMenu(std::shared_ptr prev_menu) : Menu(prev_menu) { + const float x = g_screen_width / 2.0f - WindowWidth / 2.0f; + const float y = g_screen_height / 2.0f - WindowHeight / 2.0f; + + this->AddButton(Fat32ButtonId, "FAT32", x + ButtonHorizontalInset, y + TitleGap, ButtonWidth, ButtonHeight); + this->AddButton(ExFatButtonId, "exFAT", x + ButtonHorizontalInset + ButtonWidth + ButtonHorizontalGap, y + TitleGap, ButtonWidth, ButtonHeight); + this->SetButtonSelected(ExFatButtonId, true); + } + + void ChooseExfatMenu::Update(u64 ns) { + u64 k_down = hidKeysDown(CONTROLLER_P1_AUTO); + + /* Go back if B is pressed. */ + if (k_down & KEY_B) { + ReturnToPreviousMenu(); + return; + } + + /* Take action if a button has been activated. */ + if (const Button *activated_button = this->GetActivatedButton(); activated_button != nullptr) { + switch (activated_button->id) { + case Fat32ButtonId: + g_use_exfat = false; + break; + case ExFatButtonId: + g_use_exfat = true; + break; + } + + ChangeMenu(std::make_shared(g_current_menu)); + } + + this->UpdateButtons(); + + /* Fallback on selecting the exfat button. */ + if (const Button *selected_button = this->GetSelectedButton(); k_down && selected_button == nullptr) { + this->SetButtonSelected(ExFatButtonId, true); + } + } + + void ChooseExfatMenu::Draw(NVGcontext *vg, u64 ns) { + const float x = g_screen_width / 2.0f - WindowWidth / 2.0f; + const float y = g_screen_height / 2.0f - WindowHeight / 2.0f; + + DrawWindow(vg, "Select driver variant", x, y, WindowWidth, WindowHeight); + this->DrawButtons(vg, ns); + } + + InstallUpdateMenu::InstallUpdateMenu(std::shared_ptr prev_menu) : Menu(prev_menu), m_install_state(InstallState::NeedsDraw), m_progress_percent(0.0f) { + const float x = g_screen_width / 2.0f - WindowWidth / 2.0f; + const float y = g_screen_height / 2.0f - WindowHeight / 2.0f; + + /* Add buttons. */ + this->AddButton(ShutdownButtonId, "Shutdown", x + HorizontalGap, y + WindowHeight - BottomGap - ButtonHeight, ButtonWidth, ButtonHeight); + this->AddButton(RebootButtonId, "Reboot", x + HorizontalGap + ButtonWidth + ButtonHorizontalGap, y + WindowHeight - BottomGap - ButtonHeight, ButtonWidth, ButtonHeight); + this->SetButtonEnabled(ShutdownButtonId, false); + this->SetButtonEnabled(RebootButtonId, false); + } + + void InstallUpdateMenu::MarkForReboot() { + this->SetButtonEnabled(ShutdownButtonId, true); + this->SetButtonEnabled(RebootButtonId, true); + this->SetButtonSelected(RebootButtonId, true); + m_install_state = InstallState::AwaitingReboot; + } + + Result InstallUpdateMenu::TransitionUpdateState() { + Result rc = 0; + if (m_install_state == InstallState::NeedsSetup) { + /* Setup the update. */ + if (R_FAILED(rc = amssuSetupUpdate(nullptr, UpdateTaskBufferSize, g_update_path, g_use_exfat))) { + this->LogText("Failed to setup update.\nResult: 0x%08x\n", rc); + this->MarkForReboot(); + return rc; + } + + /* Log setup completion. */ + this->LogText("Update setup complete.\n"); + m_install_state = InstallState::NeedsPrepare; + } else if (m_install_state == InstallState::NeedsPrepare) { + /* Request update preparation. */ + if (R_FAILED(rc = amssuRequestPrepareUpdate(&m_prepare_result))) { + this->LogText("Failed to request update preparation.\nResult: 0x%08x\n", rc); + this->MarkForReboot(); + return rc; + } + + /* Log awaiting prepare. */ + this->LogText("Preparing update...\n"); + m_install_state = InstallState::AwaitingPrepare; + } else if (m_install_state == InstallState::AwaitingPrepare) { + /* Check if preparation has a result. */ + if (R_FAILED(rc = asyncResultWait(&m_prepare_result, 0)) && rc != 0xea01) { + this->LogText("Failed to check update preparation result.\nResult: 0x%08x\n", rc); + this->MarkForReboot(); + return rc; + } else if (R_SUCCEEDED(rc)) { + if (R_FAILED(rc = asyncResultGet(&m_prepare_result))) { + this->LogText("Failed to prepare update.\nResult: 0x%08x\n", rc); + this->MarkForReboot(); + return rc; + } + } + + /* Check if the update has been prepared. */ + bool prepared; + if (R_FAILED(rc = amssuHasPreparedUpdate(&prepared))) { + this->LogText("Failed to check if update has been prepared.\nResult: 0x%08x\n", rc); + this->MarkForReboot(); + return rc; + } + + /* Mark for application if preparation complete. */ + if (prepared) { + this->LogText("Update preparation complete.\nApplying update...\n"); + m_install_state = InstallState::NeedsApply; + return rc; + } + + /* Check update progress. */ + NsSystemUpdateProgress update_progress = {}; + if (R_FAILED(rc = amssuGetPrepareUpdateProgress(&update_progress))) { + this->LogText("Failed to check update progress.\nResult: 0x%08x\n", rc); + this->MarkForReboot(); + return rc; + } + + /* Update progress percent. */ + if (update_progress.total_size > 0.0f) { + m_progress_percent = static_cast(update_progress.current_size) / static_cast(update_progress.total_size); + } else { + m_progress_percent = 0.0f; + } + } else if (m_install_state == InstallState::NeedsApply) { + /* Apply the prepared update. */ + if (R_FAILED(rc = amssuApplyPreparedUpdate())) { + this->LogText("Failed to apply update.\nResult: 0x%08x\n", rc); + } + + /* Log success. */ + this->LogText("Update applied successfully.\n"); + this->MarkForReboot(); + return rc; + } + + return rc; + } + + void InstallUpdateMenu::Update(u64 ns) { + /* Transition to the next update state. */ + if (m_install_state != InstallState::NeedsDraw && m_install_state != InstallState::AwaitingReboot) { + this->TransitionUpdateState(); + } + + /* Take action if a button has been activated. */ + if (const Button *activated_button = this->GetActivatedButton(); activated_button != nullptr) { + switch (activated_button->id) { + case ShutdownButtonId: + if (R_FAILED(appletRequestToShutdown())) { + spsmShutdown(false); + } + break; + case RebootButtonId: + if (R_FAILED(appletRequestToReboot())) { + spsmShutdown(true); + } + break; + } + } + + this->UpdateButtons(); + } + + void InstallUpdateMenu::Draw(NVGcontext *vg, u64 ns) { + const float x = g_screen_width / 2.0f - WindowWidth / 2.0f; + const float y = g_screen_height / 2.0f - WindowHeight / 2.0f; + + DrawWindow(vg, "Installing update", x, y, WindowWidth, WindowHeight); + DrawProgressText(vg, x + HorizontalGap, y + TitleGap, m_progress_percent); + DrawProgressBar(vg, x + HorizontalGap, y + TitleGap + ProgressTextHeight, WindowWidth - HorizontalGap * 2.0f, ProgressBarHeight, m_progress_percent); + DrawTextBackground(vg, x + HorizontalGap, y + TitleGap + ProgressTextHeight + ProgressBarHeight + VerticalGap, WindowWidth - HorizontalGap * 2.0f, TextAreaHeight); + DrawTextBlock(vg, m_log_buffer, x + HorizontalGap + TextHorizontalInset, y + TitleGap + ProgressTextHeight + ProgressBarHeight + VerticalGap + TextVerticalInset, WindowWidth - (HorizontalGap + TextHorizontalInset) * 2.0f, TextAreaHeight - TextVerticalInset * 2.0f); + + this->DrawButtons(vg, ns); + + /* We have drawn now, allow setup to occur. */ + if (m_install_state == InstallState::NeedsDraw) { + this->LogText("Beginning update setup...\n"); + m_install_state = InstallState::NeedsSetup; + } + } + + void InitializeMenu(u32 screen_width, u32 screen_height) { + /* Set the screen width and height. */ + g_screen_width = screen_width; + g_screen_height = screen_height; + + /* Change the current menu to the main menu. */ + g_current_menu = std::make_shared(); + + /* Mark as initialized. */ + g_initialized = true; + } + + void UpdateMenu(u64 ns) { + DBK_ABORT_UNLESS(g_initialized); + DBK_ABORT_UNLESS(g_current_menu != nullptr); + UpdateInput(); + g_current_menu->Update(ns); + } + + void RenderMenu(NVGcontext *vg, u64 ns) { + DBK_ABORT_UNLESS(g_initialized); + DBK_ABORT_UNLESS(g_current_menu != nullptr); + + /* Draw background. */ + DrawBackground(vg, g_screen_width, g_screen_height); + + /* Draw stars. */ + DrawStar(vg, 40.0f, 64.0f, 3.0f); + DrawStar(vg, 110.0f, 300.0f, 3.0f); + DrawStar(vg, 200.0f, 150.0f, 4.0f); + DrawStar(vg, 370.0f, 280.0f, 3.0f); + DrawStar(vg, 450.0f, 40.0f, 3.5f); + DrawStar(vg, 710.0f, 90.0f, 3.0f); + DrawStar(vg, 900.0f, 240.0f, 3.0f); + DrawStar(vg, 970.0f, 64.0f, 4.0f); + DrawStar(vg, 1160.0f, 160.0f, 3.5f); + DrawStar(vg, 1210.0f, 350.0f, 3.0f); + + g_current_menu->Draw(vg, ns); + } + + bool IsExitRequested() { + return g_exit_requested; + } + +} diff --git a/troposphere/daybreak/source/ui.hpp b/troposphere/daybreak/source/ui.hpp new file mode 100644 index 000000000..8afa43b16 --- /dev/null +++ b/troposphere/daybreak/source/ui.hpp @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2020 Adubbz + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "ams_su.h" + +namespace dbk { + + struct Button { + static constexpr u32 InvalidButtonId = -1; + + u32 id; + bool selected; + bool enabled; + char text[256]; + float x; + float y; + float w; + float h; + + inline bool IsPositionInBounds(float x, float y) { + return x >= this->x && y >= this->y && x < (this->x + this->w) && y < (this->y + this->h); + } + }; + + enum class Direction { + Up, + Down, + Left, + Right, + Invalid, + }; + + class Menu { + protected: + static constexpr size_t MaxButtons = 32; + static constexpr size_t LogBufferSize = 0x1000; + protected: + std::array, MaxButtons> m_buttons; + const std::shared_ptr m_prev_menu; + char m_log_buffer[LogBufferSize]; + protected: + void AddButton(u32 id, const char *text, float x, float y, float w, float h); + void SetButtonSelected(u32 id, bool selected); + void DeselectAllButtons(); + void SetButtonEnabled(u32 id, bool enabled); + + Button *GetButton(u32 id); + Button *GetSelectedButton(); + Button *GetClosestButtonToSelection(Direction direction); + Button *GetTouchedButton(); + Button *GetActivatedButton(); + + void UpdateButtons(); + void DrawButtons(NVGcontext *vg, u64 ns); + + void LogText(const char *format, ...); + public: + Menu(std::shared_ptr prev_menu) : m_buttons({}), m_prev_menu(prev_menu), m_log_buffer{} { /* ... */ } + + std::shared_ptr GetPrevMenu(); + virtual void Update(u64 ns) = 0; + virtual void Draw(NVGcontext *vg, u64 ns) = 0; + }; + + class MainMenu : public Menu { + private: + static constexpr u32 InstallButtonId = 0; + static constexpr u32 ExitButtonId = 1; + + static constexpr float WindowWidth = 400.0f; + static constexpr float WindowHeight = 240.0f; + static constexpr float TitleGap = 90.0f; + static constexpr float ButtonHorizontalPadding = 20.0f; + static constexpr float ButtonHeight = 60.0f; + static constexpr float ButtonVerticalGap = 10.0f; + public: + MainMenu(); + + virtual void Update(u64 ns) override; + virtual void Draw(NVGcontext *vg, u64 ns) override; + }; + + class FileMenu : public Menu { + private: + struct FileEntry { + char name[FS_MAX_PATH]; + }; + private: + static constexpr size_t MaxFileRows = 11; + + static constexpr float WindowWidth = 1200.0f; + static constexpr float WindowHeight = 680.0f; + static constexpr float TitleGap = 90.0f; + static constexpr float TextBackgroundOffset = 20.0f; + static constexpr float FileRowHeight = 40.0f; + static constexpr float FileRowGap = 10.0f; + static constexpr float FileRowHorizontalInset = 10.0f; + static constexpr float FileListHeight = MaxFileRows * (FileRowHeight + FileRowGap); + private: + char m_root[FS_MAX_PATH]; + std::vector m_file_entries; + u32 m_current_index; + float m_scroll_offset; + float m_touch_start_scroll_offset; + bool m_touch_finalize_selection; + + Result PopulateFileEntries(); + bool IsSelectionVisible(); + void ScrollToSelection(); + bool IsEntryTouched(u32 i); + void UpdateTouches(); + void FinalizeSelection(); + public: + FileMenu(std::shared_ptr prev_menu, const char *root); + + virtual void Update(u64 ns) override; + virtual void Draw(NVGcontext *vg, u64 ns) override; + }; + + class ValidateUpdateMenu : public Menu { + private: + static constexpr u32 BackButtonId = 0; + static constexpr u32 ContinueButtonId = 1; + + static constexpr float WindowWidth = 600.0f; + static constexpr float WindowHeight = 600.0f; + static constexpr float TitleGap = 90.0f; + static constexpr float BottomGap = 20.0f; + static constexpr float HorizontalGap = 20.0f; + static constexpr float TextAreaHeight = 410.0f; + static constexpr float TextHorizontalInset = 6.0f; + static constexpr float TextVerticalInset = 6.0f; + static constexpr float ButtonHeight = 60.0f; + static constexpr float ButtonHorizontalGap = 10.0f; + static constexpr float ButtonWidth = (WindowWidth - HorizontalGap * 2.0f) / 2.0f - ButtonHorizontalGap; + private: + AmsSuUpdateInformation m_update_info; + AmsSuUpdateValidationInfo m_validation_info; + bool m_has_drawn; + bool m_has_info; + bool m_has_validated; + + Result GetUpdateInformation(); + void ValidateUpdate(); + public: + ValidateUpdateMenu(std::shared_ptr prev_menu); + + virtual void Update(u64 ns) override; + virtual void Draw(NVGcontext *vg, u64 ns) override; + }; + + class ChooseExfatMenu : public Menu { + private: + static constexpr u32 Fat32ButtonId = 0; + static constexpr u32 ExFatButtonId = 1; + + static constexpr float WindowWidth = 600.0f; + static constexpr float WindowHeight = 180.0f; + static constexpr float TitleGap = 90.0f; + static constexpr float ButtonHeight = 60.0f; + static constexpr float ButtonHorizontalInset = 20.0f; + static constexpr float ButtonHorizontalGap = 10.0f; + static constexpr float ButtonWidth = (WindowWidth - ButtonHorizontalInset * 2.0f) / 2.0f - ButtonHorizontalGap; + public: + ChooseExfatMenu(std::shared_ptr prev_menu); + + virtual void Update(u64 ns) override; + virtual void Draw(NVGcontext *vg, u64 ns) override; + }; + + class InstallUpdateMenu : public Menu { + private: + enum class InstallState { + NeedsDraw, + NeedsSetup, + NeedsPrepare, + AwaitingPrepare, + NeedsApply, + AwaitingReboot, + }; + private: + static constexpr u32 ShutdownButtonId = 0; + static constexpr u32 RebootButtonId = 1; + + static constexpr float WindowWidth = 600.0f; + static constexpr float WindowHeight = 600.0f; + static constexpr float TitleGap = 120.0f; + static constexpr float BottomGap = 20.0f; + static constexpr float HorizontalGap = 20.0f; + static constexpr float ProgressTextHeight = 20.0f; + static constexpr float ProgressBarHeight = 30.0f; + static constexpr float VerticalGap = 10.0f; + static constexpr float TextAreaHeight = 320.0f; + static constexpr float TextHorizontalInset = 6.0f; + static constexpr float TextVerticalInset = 6.0f; + static constexpr float ButtonHeight = 60.0f; + static constexpr float ButtonHorizontalGap = 10.0f; + static constexpr float ButtonWidth = (WindowWidth - HorizontalGap * 2.0f) / 2.0f - ButtonHorizontalGap; + + static constexpr size_t UpdateTaskBufferSize = 0x100000; + private: + InstallState m_install_state; + AsyncResult m_prepare_result; + float m_progress_percent; + + void MarkForReboot(); + Result TransitionUpdateState(); + public: + InstallUpdateMenu(std::shared_ptr prev_menu); + + virtual void Update(u64 ns) override; + virtual void Draw(NVGcontext *vg, u64 ns) override; + }; + + void InitializeMenu(u32 screen_width, u32 screen_height); + void UpdateMenu(u64 ns); + void RenderMenu(NVGcontext *vg, u64 ns); + bool IsExitRequested(); + +} diff --git a/troposphere/daybreak/source/ui_util.cpp b/troposphere/daybreak/source/ui_util.cpp new file mode 100644 index 000000000..aa88c4971 --- /dev/null +++ b/troposphere/daybreak/source/ui_util.cpp @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2020 Adubbz + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "ui_util.hpp" +#include +#include + +namespace dbk { + + namespace { + + constexpr const char *SwitchStandardFont = "switch-standard"; + constexpr float WindowCornerRadius = 20.0f; + constexpr float TextAreaCornerRadius = 10.0f; + constexpr float ButtonCornerRaidus = 3.0f; + + NVGcolor GetSelectionRGB2(u64 ns) { + /* Calculate the rgb values for the breathing colour effect. */ + const double t = static_cast(ns) / 1'000'000'000.0d; + const float d = -0.5 * cos(3.0f*t) + 0.5f; + const int r2 = 83 + (float)(144 - 83) * (d * 0.7f + 0.3f); + const int g2 = 71 + (float)(185 - 71) * (d * 0.7f + 0.3f); + const int b2 = 185 + (float)(217 - 185) * (d * 0.7f + 0.3f); + return nvgRGB(r2, g2, b2); + } + + } + + void DrawStar(NVGcontext *vg, float x, float y, float width) { + nvgBeginPath(vg); + nvgEllipse(vg, x, y, width, width * 3.0f); + nvgEllipse(vg, x, y, width * 3.0f, width); + nvgFillColor(vg, nvgRGB(65, 71, 115)); + nvgFill(vg); + } + + void DrawBackground(NVGcontext *vg, float w, float h) { + /* Draw the background gradient. */ + const NVGpaint bg_paint = nvgLinearGradient(vg, w / 2.0f, 0, w / 2.0f, h + 20.0f, nvgRGB(20, 24, 50), nvgRGB(46, 57, 127)); + nvgBeginPath(vg); + nvgRect(vg, 0, 0, w, h); + nvgFillPaint(vg, bg_paint); + nvgFill(vg); + } + + void DrawWindow(NVGcontext *vg, const char *title, float x, float y, float w, float h) { + /* Draw the window background. */ + const NVGpaint window_bg_paint = nvgLinearGradient(vg, x + w / 2.0f, y, x + w / 2.0f, y + h + h / 4.0f, nvgRGB(255, 255, 255), nvgRGB(188, 214, 234)); + nvgBeginPath(vg); + nvgRoundedRect(vg, x, y, w, h, WindowCornerRadius); + nvgFillPaint(vg, window_bg_paint); + nvgFill(vg); + + /* Draw the shadow surrounding the window. */ + NVGpaint shadowPaint = nvgBoxGradient(vg, x, y + 2, w, h, WindowCornerRadius * 2, 10, nvgRGBA(0, 0, 0, 128), nvgRGBA(0, 0, 0, 0)); + nvgBeginPath(vg); + nvgRect(vg, x - 10, y - 10, w + 20, h + 30); + nvgRoundedRect(vg, x, y, w, h, WindowCornerRadius); + nvgPathWinding(vg, NVG_HOLE); + nvgFillPaint(vg, shadowPaint); + nvgFill(vg); + + /* Setup the font. */ + nvgFontSize(vg, 32.0f); + nvgFontFace(vg, SwitchStandardFont); + nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE); + nvgFillColor(vg, nvgRGB(0, 0, 0)); + + /* Draw the title. */ + const float tw = nvgTextBounds(vg, 0, 0, title, nullptr, nullptr); + nvgText(vg, x + w * 0.5f - tw * 0.5f, y + 40.0f, title, nullptr); + } + + void DrawButton(NVGcontext *vg, const char *text, float x, float y, float w, float h, ButtonStyle style, u64 ns) { + /* Fill the background if selected. */ + if (style == ButtonStyle::StandardSelected || style == ButtonStyle::FileSelectSelected) { + NVGpaint bg_paint = nvgLinearGradient(vg, x, y + h / 2.0f, x + w, y + h / 2.0f, nvgRGB(83, 71, 185), GetSelectionRGB2(ns)); + nvgBeginPath(vg); + nvgRoundedRect(vg, x, y, w, h, ButtonCornerRaidus); + nvgFillPaint(vg, bg_paint); + nvgFill(vg); + } + + /* Draw the shadow surrounding the button. */ + if (style == ButtonStyle::Standard || style == ButtonStyle::StandardSelected || style == ButtonStyle::StandardDisabled || style == ButtonStyle::FileSelectSelected) { + const unsigned char shadow_color = style == ButtonStyle::Standard ? 128 : 64; + NVGpaint shadow_paint = nvgBoxGradient(vg, x, y, w, h, ButtonCornerRaidus, 5, nvgRGBA(0, 0, 0, shadow_color), nvgRGBA(0, 0, 0, 0)); + nvgBeginPath(vg); + nvgRect(vg, x - 10, y - 10, w + 20, h + 30); + nvgRoundedRect(vg, x, y, w, h, ButtonCornerRaidus); + nvgPathWinding(vg, NVG_HOLE); + nvgFillPaint(vg, shadow_paint); + nvgFill(vg); + } + + /* Setup the font. */ + nvgFontSize(vg, 20.0f); + nvgFontFace(vg, SwitchStandardFont); + nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE); + + /* Set the text colour. */ + if (style == ButtonStyle::StandardSelected || style == ButtonStyle::FileSelectSelected) { + nvgFillColor(vg, nvgRGB(255, 255, 255)); + } else { + const unsigned char alpha = style == ButtonStyle::StandardDisabled ? 64 : 255; + nvgFillColor(vg, nvgRGBA(0, 0, 0, alpha)); + } + + /* Draw the button text. */ + const float tw = nvgTextBounds(vg, 0, 0, text, nullptr, nullptr); + + if (style == ButtonStyle::Standard || style == ButtonStyle::StandardSelected || style == ButtonStyle::StandardDisabled) { + nvgText(vg, x + w * 0.5f - tw * 0.5f, y + h * 0.5f, text, nullptr); + } else { + nvgText(vg, x + 10.0f, y + h * 0.5f, text, nullptr); + } + } + + void DrawTextBackground(NVGcontext *vg, float x, float y, float w, float h) { + nvgBeginPath(vg); + nvgRoundedRect(vg, x, y, w, h, TextAreaCornerRadius); + nvgFillColor(vg, nvgRGBA(0, 0, 0, 16)); + nvgFill(vg); + } + + void DrawProgressText(NVGcontext *vg, float x, float y, float progress) { + char progress_text[32] = {}; + snprintf(progress_text, sizeof(progress_text)-1, "%d%% complete", static_cast(progress * 100.0f)); + + nvgFontSize(vg, 24.0f); + nvgFontFace(vg, SwitchStandardFont); + nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE); + nvgFillColor(vg, nvgRGB(0, 0, 0)); + nvgText(vg, x, y, progress_text, nullptr); + } + + void DrawProgressBar(NVGcontext *vg, float x, float y, float w, float h, float progress) { + /* Draw the progress bar background. */ + nvgBeginPath(vg); + nvgRoundedRect(vg, x, y, w, h, WindowCornerRadius); + nvgFillColor(vg, nvgRGBA(0, 0, 0, 128)); + nvgFill(vg); + + /* Draw the progress bar fill. */ + if (progress > 0.0f) { + NVGpaint progress_fill_paint = nvgLinearGradient(vg, x, y + 0.5f * h, x + w, y + 0.5f * h, nvgRGB(83, 71, 185), nvgRGB(144, 185, 217)); + nvgBeginPath(vg); + nvgRoundedRect(vg, x, y, WindowCornerRadius + (w - WindowCornerRadius) * progress, h, WindowCornerRadius); + nvgFillPaint(vg, progress_fill_paint); + nvgFill(vg); + } + } + + void DrawTextBlock(NVGcontext *vg, const char *text, float x, float y, float w, float h) { + /* Save state and scissor. */ + nvgSave(vg); + nvgScissor(vg, x, y, w, h); + + /* Configure the text. */ + nvgFontSize(vg, 18.0f); + nvgFontFace(vg, SwitchStandardFont); + nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_TOP); + nvgFillColor(vg, nvgRGB(0, 0, 0)); + + /* Determine the bounds of the text box. */ + float bounds[4]; + nvgTextBoxBounds(vg, 0, 0, w, text, nullptr, bounds); + + /* Adjust the y to only show the last part of the text that fits. */ + float y_adjustment = 0.0f; + if (bounds[3] > h) { + y_adjustment = bounds[3] - h; + } + + /* Draw the text box and restore state. */ + nvgTextBox(vg, x, y - y_adjustment, w, text, nullptr); + nvgRestore(vg); + } + +} diff --git a/troposphere/daybreak/source/ui_util.hpp b/troposphere/daybreak/source/ui_util.hpp new file mode 100644 index 000000000..4522760ab --- /dev/null +++ b/troposphere/daybreak/source/ui_util.hpp @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 Adubbz + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#pragma once +#include +#include + +namespace dbk { + + enum class ButtonStyle { + Standard, + StandardSelected, + StandardDisabled, + FileSelect, + FileSelectSelected, + }; + + void DrawStar(NVGcontext *vg, float x, float y, float width); + void DrawBackground(NVGcontext *vg, float w, float h); + void DrawWindow(NVGcontext *vg, const char *title, float x, float y, float w, float h); + void DrawButton(NVGcontext *vg, const char *text, float x, float y, float w, float h, ButtonStyle style, u64 ns); + void DrawTextBackground(NVGcontext *vg, float x, float y, float w, float h); + void DrawProgressText(NVGcontext *vg, float x, float y, float progress); + void DrawProgressBar(NVGcontext *vg, float x, float y, float w, float h, float progress); + void DrawTextBlock(NVGcontext *vg, const char *text, float x, float y, float w, float h); + +}