From 6c3a06f53d9caebadfb0aeed9bb94119012010a7 Mon Sep 17 00:00:00 2001 From: illusion0001 <37698908+illusion0001@users.noreply.github.com> Date: Tue, 4 Jul 2023 14:28:44 -0500 Subject: [PATCH] New Plugin: FrameTime Logger --- .github/README.md | 14 +- plugin_src/frame_logger/Makefile | 99 ++++++++++ plugin_src/frame_logger/source/main.c | 249 ++++++++++++++++++++++++++ 3 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 plugin_src/frame_logger/Makefile create mode 100644 plugin_src/frame_logger/source/main.c diff --git a/.github/README.md b/.github/README.md index 09afda39..ed15cd64 100644 --- a/.github/README.md +++ b/.github/README.md @@ -13,7 +13,7 @@ Plugins allows you to customize your games like never before! While we make every effort to deliver high quality products, we do not guarantee that our products are free from defects. Our software is provided **as is** and you use the software at your own risk. -# Getting Started +# Quick Start - Load [GoldHEN 2.3](https://github.com/GoldHEN/GoldHEN/releases/latest) or newer on your PS4. - Enable option to load plugins in Plugins Menu. @@ -94,6 +94,18 @@ Author(s): Removes framerate limit for games using system function `sceVideoOutSetFlipRate`. +### FrameTime Logger + +Plugin filename: `frame_logger.prx` + +Author(s): +- [illusion](https://github.com/illusion0001) + + - Log frametime statistics. + - Press `L3 + L1 + Triangle` to start capturing data. + - Press `L3 + R3 + L1 + R1 + Square` to stop plugin. + - Logs data to `/data/frame_logger/`. + ### GamePad helper Plugin Plugin filename: `gamepad_helper.prx` diff --git a/plugin_src/frame_logger/Makefile b/plugin_src/frame_logger/Makefile new file mode 100644 index 00000000..f4e62b64 --- /dev/null +++ b/plugin_src/frame_logger/Makefile @@ -0,0 +1,99 @@ +# Library metadata. + +DEBUG_FLAGS = -D__FINAL__=1 +LOG_TYPE = -D__USE_PRINTF__ +BUILD_TYPE = _final + +ifeq ($(DEBUG),1) + DEBUG_FLAGS = -D__FINAL__=0 + BUILD_TYPE = _debug +endif + +TYPE := $(BUILD_TYPE) +FINAL := $(DEBUG_FLAGS) +BUILD_FOLDER := $(shell pwd)/../../bin/plugins +OUTPUT_PRX := $(shell basename $(CURDIR)) +TARGET := $(BUILD_FOLDER)/prx$(TYPE)/$(OUTPUT_PRX) +TARGET_ELF := $(BUILD_FOLDER)/elf$(TYPE)/$(OUTPUT_PRX) +TARGETSTUB := $(OUTPUT_PRX).so + +# Libraries linked into the ELF. +LIBS := -lSceLibcInternal -lSceGnmDriver -lScePad -lGoldHEN_Hook -lkernel -lSceSysmodule -lSceSystemService -lSceUserService + +EXTRAFLAGS := $(DEBUG_FLAGS) $(LOG_TYPE) -fcolor-diagnostics -Wall + +# You likely won't need to touch anything below this point. +# Root vars +TOOLCHAIN := $(OO_PS4_TOOLCHAIN) +GH_SDK := $(GOLDHEN_SDK) +PROJDIR := ../$(shell basename $(CURDIR))/source +INTDIR := ../$(shell basename $(CURDIR))/build +INCLUDEDIR := ../$(shell basename $(CURDIR))/include +COMMON_DIR := ../../common + +# Define objects to build +CFILES := $(wildcard $(PROJDIR)/*.c) +CPPFILES := $(wildcard $(PROJDIR)/*.cpp) +COMMONFILES := $(wildcard $(COMMONDIR)/*.cpp) +OBJS := $(patsubst $(PROJDIR)/%.c, $(INTDIR)/%.o, $(CFILES)) $(patsubst $(PROJDIR)/%.cpp, $(INTDIR)/%.o, $(CPPFILES)) $(patsubst $(COMMONDIR)/%.cpp, $(INTDIR)/%.o, $(COMMONFILES)) +STUBOBJS := $(patsubst $(PROJDIR)/%.c, $(INTDIR)/%.o, $(CFILES)) $(patsubst $(PROJDIR)/%.cpp, $(INTDIR)/%.o.stub, $(CPPFILES)) $(patsubst $(COMMONDIR)/%.cpp, $(INTDIR)/%.o.stub, $(COMMONFILES)) + +# Define final C/C++ flags +CFLAGS := $(FINAL) --target=x86_64-pc-freebsd12-elf -fPIC -funwind-tables -c $(EXTRAFLAGS) -isysroot $(TOOLCHAIN) -isystem $(TOOLCHAIN)/include -I$(GH_SDK)/include -I$(INCLUDEDIR) -I$(COMMON_DIR) $(O_FLAG) +CXXFLAGS := $(CFLAGS) -isystem $(TOOLCHAIN)/$(INCLUDEDIR)/c++/v1 +LDFLAGS := -m elf_x86_64 -pie --script $(TOOLCHAIN)/link.x -e _init --eh-frame-hdr -L$(TOOLCHAIN)/lib -L$(GH_SDK) $(LIBS) + +# Create the intermediate directory incase it doesn't already exist. +_unused := $(shell mkdir -p $(INTDIR)) + +# Check for linux vs macOS and account for clang/ld path +UNAME_S := $(shell uname -s) + +ifeq ($(UNAME_S),Linux) + CC := clang + CCX := clang++ + LD := ld.lld + CDIR := linux +endif +ifeq ($(UNAME_S),Darwin) + CC := /usr/local/opt/llvm/bin/clang + CCX := /usr/local/opt/llvm/bin/clang++ + LD := /usr/local/opt/llvm/bin/ld.lld + CDIR := macos +endif + +$(TARGET): $(INTDIR) $(OBJS) + $(LD) $(GH_SDK)/build/crtprx.o $(INTDIR)/*.o -o $(TARGET_ELF).elf $(LDFLAGS) + $(TOOLCHAIN)/bin/$(CDIR)/create-fself -in=$(TARGET_ELF).elf -out=$(TARGET_ELF).oelf --lib=$(TARGET).prx --paid 0x3800000000000011 + +$(TARGETSTUB): $(INTDIR) $(STUBOBJS) + $(CC) $(INTDIR)/*.o.stub -o $(TARGETSTUB) -target x86_64-pc-linux-gnu -shared -fuse-ld=lld -ffreestanding -nostdlib -fno-builtin -L$(TOOLCHAIN)/lib $(LIBS) + +$(INTDIR)/%.o: $(PROJDIR)/%.c + $(CC) $(CFLAGS) -o $@ $< + +$(INTDIR)/%.o: $(PROJDIR)/%.cpp + $(CCX) $(CXXFLAGS) -o $@ $< + +$(INTDIR)/%.o.stub: $(PROJDIR)/%.c + $(CC) -target x86_64-pc-linux-gnu -ffreestanding -nostdlib -fno-builtin -fPIC $(O_FLAG) -s -c -o $@ $< + +$(INTDIR)/%.o.stub: $(PROJDIR)/%.cpp + $(CCX) -target x86_64-pc-linux-gnu -ffreestanding -nostdlib -fno-builtin -fPIC $(O_FLAG) -s -c -o $@ $< + +build-info: + $(shell echo "#define GIT_COMMIT \"$(shell git rev-parse HEAD)\"" > $(COMMON_DIR)/git_ver.h) + $(shell echo "#define GIT_VER \"$(shell git branch --show-current)\"" >> $(COMMON_DIR)/git_ver.h) + $(shell echo "#define GIT_NUM $(shell git rev-list HEAD --count)" >> $(COMMON_DIR)/git_ver.h) + $(shell echo "#define BUILD_DATE \"$(shell date '+%b %d %Y @ %T')\"" >> $(COMMON_DIR)/git_ver.h) + +plugin_common: + $(CC) $(CFLAGS) -o $(INTDIR)/plugin_common.o $(COMMON_DIR)/plugin_common.c + +.PHONY: clean +.DEFAULT_GOAL := all + +all: build-info plugin_common $(TARGET) + +clean: + rm -rf $(TARGET) $(TARGETSTUB) $(INTDIR) $(OBJS) diff --git a/plugin_src/frame_logger/source/main.c b/plugin_src/frame_logger/source/main.c new file mode 100644 index 00000000..c6477c02 --- /dev/null +++ b/plugin_src/frame_logger/source/main.c @@ -0,0 +1,249 @@ +// FrameTime Logger: Log frametime statistics. +// Author: illusion0001 @ https://github.com/illusion0001 +// Repository: https://github.com/GoldHEN/GoldHEN_Plugins_Repository + +#include +#include +#include +#include +#include +#include +#include +#include "plugin_common.h" + +#define PLUGIN_NAME "frame_logger" +#define LOG_FOLDER "/data/" PLUGIN_NAME + +attr_public const char *g_pluginName = PLUGIN_NAME; +attr_public const char *g_pluginDesc = "Log frametime statistics."; +attr_public const char *g_pluginAuth = "illusion"; +attr_public u32 g_pluginVersion = 0x00000100; // 1.00 + +int32_t sceGnmSubmitAndFlipCommandBuffers(uint32_t count, void *dcbGpuAddrs[], uint32_t *dcbSizesInBytes, void *ccbGpuAddrs[], uint32_t *ccbSizesInBytes, uint32_t videoOutHandle, uint32_t displayBufferIndex, uint32_t flipMode, int64_t flipArg); + +HOOK_INIT(sceGnmSubmitAndFlipCommandBuffers); + +FILE *g_LogFILE = NULL; +bool g_isRecording = false; +uint64_t g_TimeStart = 0; +uint64_t g_CurrentDelta = 0; +double g_TscTick = 0; +bool g_GnmHook = false; +bool g_RunningThread = false; + +void doStats(void) +{ + if (!g_GnmHook) + { + return; + } + uint64_t current_time = sceKernelGetProcessTimeCounter(); + if (g_isRecording && g_LogFILE) + { + fprintf(g_LogFILE, "%lf,%lf\n", ((current_time - g_TimeStart) / g_TscTick), ((current_time - g_CurrentDelta) / g_TscTick)); + } + g_CurrentDelta = current_time; +} + +int32_t sceGnmSubmitAndFlipCommandBuffers_hook(uint32_t count, void *dcbGpuAddrs[], uint32_t *dcbSizesInBytes, void *ccbGpuAddrs[], uint32_t *ccbSizesInBytes, uint32_t videoOutHandle, uint32_t displayBufferIndex, uint32_t flipMode, int64_t flipArg) +{ + if (!g_GnmHook) + { + g_GnmHook = true; + } + doStats(); + return HOOK_CONTINUE(sceGnmSubmitAndFlipCommandBuffers, + int32_t(*)(uint32_t, void **, uint32_t *, void **, uint32_t *, uint32_t, uint32_t, uint32_t, int64_t), + count, dcbGpuAddrs, dcbSizesInBytes, ccbGpuAddrs, ccbSizesInBytes, videoOutHandle, displayBufferIndex, flipMode, flipArg); +} + +struct tm get_local_time(void) +{ + int32_t tz_offset = 0; + int32_t tz_dst = 0; + int32_t ret = 0; + + if ((ret = sceSystemServiceParamGetInt(ORBIS_SYSTEM_SERVICE_PARAM_ID_TIME_ZONE, &tz_offset)) < 0) + { + final_printf("Failed to obtain ORBIS_SYSTEM_SERVICE_PARAM_ID_TIME_ZONE! Setting timezone offset to 0\n"); + final_printf("sceSystemServiceParamGetInt: 0x%08x\n", ret); + tz_offset = 0; + } + + if ((ret = sceSystemServiceParamGetInt(ORBIS_SYSTEM_SERVICE_PARAM_ID_SUMMERTIME, &tz_dst)) < 0) + { + final_printf("Failed to obtain ORBIS_SYSTEM_SERVICE_PARAM_ID_SUMMERTIME! Setting timezone daylight time savings to 0\n"); + final_printf("sceSystemServiceParamGetInt: 0x%08x\n", ret); + tz_dst = 0; + } + + time_t modifiedTime = time(NULL) + ((tz_offset + (tz_dst * 60)) * 60); + return (*gmtime(&modifiedTime)); +} + +void toggleRecording(void) +{ + g_isRecording = !g_isRecording; + if (!g_GnmHook) + { + NotifyStatic(TEX_ICON_SYSTEM, "g_GnmHook is false! cannot start data capture\nPlugin (" PLUGIN_NAME ") will now quit."); + g_isRecording = false; + g_RunningThread = false; + return; + } + if (g_isRecording) + { + sceKernelMkdir(LOG_FOLDER "/", 0777); + char LogFilePath[MAX_PATH_] = {0}; + g_LogFILE = NULL; + struct tm t = get_local_time(); + snprintf(LogFilePath, sizeof(LogFilePath), LOG_FOLDER "/" PLUGIN_NAME "-data-%d-%02d-%02d_%02d-%02d-%02d.csv", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec); + g_LogFILE = fopen(LogFilePath, "w"); + if (g_LogFILE) + { + g_TscTick = (double)sceKernelGetProcessTimeCounterFrequency(); + g_TimeStart = sceKernelGetProcessTimeCounter(); + g_CurrentDelta = g_TimeStart; + Notify(TEX_ICON_SYSTEM, "Recording start:\n%s", LogFilePath); + fputs("//Ignore=true\n" + "TimeInSeconds,msBetweenPresents\n", + g_LogFILE); + } + else + { + Notify(TEX_ICON_SYSTEM, "Failed to create:\n%s", LogFilePath); + g_isRecording = false; + } + } + else + { + fflush(g_LogFILE); + fclose(g_LogFILE); + NotifyStatic(TEX_ICON_SYSTEM, "Recording stop"); + } +} + +bool checkRecordingButton(OrbisPadData *pData) +{ + return (pData->buttons & ORBIS_PAD_BUTTON_L3) && + (pData->buttons & ORBIS_PAD_BUTTON_L1) && + (pData->buttons & ORBIS_PAD_BUTTON_TRIANGLE); +} + +bool checkKillButton(OrbisPadData *pData) +{ + return (pData->buttons & ORBIS_PAD_BUTTON_L3) && + (pData->buttons & ORBIS_PAD_BUTTON_R3) && + (pData->buttons & ORBIS_PAD_BUTTON_L1) && + (pData->buttons & ORBIS_PAD_BUTTON_R1) && + (pData->buttons & ORBIS_PAD_BUTTON_SQUARE); +} + +int32_t GetHandle(void) +{ + int32_t padHandle = 0; + int32_t userID = 0; + OrbisUserServiceInitializeParams param; + param.priority = ORBIS_KERNEL_PRIO_FIFO_LOWEST; + sceUserServiceInitialize(¶m); + sceUserServiceGetInitialUser(&userID); + if ((padHandle = scePadOpen(userID, 0, 0, 0)) == ORBIS_PAD_ERROR_ALREADY_OPENED) + { + padHandle = scePadGetHandle(userID, 0, 0); + } + else if ((padHandle = scePadOpen(userID, 0, 0, 0)) < 0) + { + final_printf("scePadOpen: return 0x%08x\n", padHandle); + padHandle = 0; + } + return padHandle; +} + +void *frame_logger_input_thread(void *args) +{ + final_printf("%s: Started\n", __func__); + final_printf("%s: Waiting 10 seconds to start input lisener\n", __func__); + sleep(10); + int32_t ret = 0; + bool prevTogglePressed = false; + if ((ret = scePadInit() < 0)) + { + NotifyStatic(TEX_ICON_SYSTEM, "Failed to init pad"); + g_RunningThread = false; + } + OrbisPadData pData; + final_printf("Start listening for controller inputs\n"); + while (g_RunningThread) + { + int32_t PadHandle = GetHandle(); + int32_t ret = scePadReadState(PadHandle, &pData); + debug_printf("scePadReadState: 0x%08x\n", ret); + if (ret == 0 && PadHandle > 0 && pData.connected) + { + bool currentTogglePressed = checkRecordingButton(&pData); + if (currentTogglePressed && !prevTogglePressed) + { + toggleRecording(); + } + prevTogglePressed = currentTogglePressed; + if (checkKillButton(&pData)) + { + NotifyStatic(TEX_ICON_SYSTEM, "User requested exit for Plugin (" PLUGIN_NAME ")"); + g_RunningThread = false; + } + } + else + { + final_printf(STRINGIFY(PadHandle)": 0x%08x\n", PadHandle); + final_printf(STRINGIFY(ret)": 0x%08x\n", ret); + final_printf(STRINGIFY(pData.connected)": 0x%02x (%s)\n", pData.connected, pData.connected ? "true" : "false"); + } + // periodically flush the stream every second to avoid data loss + if (g_LogFILE) + { +#if (__FINAL__) == 0 + int32_t flush_ret = fflush(g_LogFILE); + debug_printf("fflush: 0x%08x\n", flush_ret); +#else + fflush(g_LogFILE); +#endif + } + sleep(1); + } + final_printf("%s: Exit\n", __func__); + scePthreadExit(NULL); + return NULL; +} + +s32 attr_public plugin_load(s32 argc, const char *argv[]) +{ + final_printf("[GoldHEN] <%s\\Ver.0x%08x> %s\n", g_pluginName, g_pluginVersion, __func__); + final_printf("[GoldHEN] Plugin Author(s): %s\n", g_pluginAuth); + boot_ver(); + g_GnmHook = false; + g_RunningThread = true; + g_TimeStart = sceKernelGetProcessTimeCounter(); + g_TscTick = (double)sceKernelGetProcessTimeCounterFrequency(); + + OrbisPthread thread; + scePthreadCreate(&thread, NULL, frame_logger_input_thread, NULL, STRINGIFY(frame_logger_input_thread)); + HOOK32(sceGnmSubmitAndFlipCommandBuffers); + return 0; +} + +s32 attr_public plugin_unload(s32 argc, const char *argv[]) +{ + final_printf("[GoldHEN] <%s\\Ver.0x%08x> %s\n", g_pluginName, g_pluginVersion, __func__); + UNHOOK(sceGnmSubmitAndFlipCommandBuffers); + return 0; +} + +s32 attr_module_hidden module_start(s64 argc, const void *args) +{ + return 0; +} + +s32 attr_module_hidden module_stop(s64 argc, const void *args) +{ + return 0; +}