From 7b742e183864c508b152ad976a83f9bdb5e174f2 Mon Sep 17 00:00:00 2001 From: tGecko Date: Mon, 30 Dec 2024 21:18:06 +0100 Subject: [PATCH] SDL to framebuffer overlay + CPU clock hotkeys (#1740) SDL to framebuffer overlay + CPU clock hotkeys --------- Co-authored-by: Aemiii91 <44569252+Aemiii91@users.noreply.github.com> Co-authored-by: Kevin Weichel --- .gitignore | 2 + Makefile | 2 + src/batmon/batmon.c | 5 +- src/common/system/display.h | 266 ++++++++++++++++++---- src/common/system/osd.h | 312 ++++++++++++++++++-------- src/common/system/screenshot.h | 4 +- src/common/utils/log.c | 7 +- src/common/utils/process.h | 27 +++ src/common/utils/timer.h | 34 +++ src/cpuclock/Makefile | 8 + src/cpuclock/cpuclock.c | 145 ++++++++++++ src/keymon/Makefile | 2 +- src/keymon/keymon.c | 119 +++++++++- src/tweaks/tweaks.c | 3 +- static/build/.tmp_update/bin/cpuclock | Bin 18068 -> 0 bytes 15 files changed, 792 insertions(+), 144 deletions(-) create mode 100644 src/common/utils/timer.h create mode 100644 src/cpuclock/Makefile create mode 100644 src/cpuclock/cpuclock.c delete mode 100644 static/build/.tmp_update/bin/cpuclock diff --git a/.gitignore b/.gitignore index fa16f684d..861c60aa7 100644 --- a/.gitignore +++ b/.gitignore @@ -66,3 +66,5 @@ src/**/tree !tree/ src/**/pippi !pippi/ +src/**/cpuclock +!cpuclock/ diff --git a/Makefile b/Makefile index 11329bf03..ee9e018f4 100644 --- a/Makefile +++ b/Makefile @@ -141,6 +141,8 @@ core: $(CACHE)/.setup @cd $(SRC_DIR)/sendUDP && BUILD_DIR=$(BIN_DIR) make @cd $(SRC_DIR)/tree && BUILD_DIR=$(BIN_DIR) make @cd $(SRC_DIR)/pippi && BUILD_DIR=$(BIN_DIR) make + @cd $(SRC_DIR)/cpuclock && BUILD_DIR=$(BIN_DIR) make + # Build dependencies for installer @mkdir -p $(INSTALLER_DIR)/bin @cd $(SRC_DIR)/installUI && BUILD_DIR=$(INSTALLER_DIR)/bin/ VERSION=$(VERSION) make diff --git a/src/batmon/batmon.c b/src/batmon/batmon.c index e9ed9a9fb..b6c196b62 100644 --- a/src/batmon/batmon.c +++ b/src/batmon/batmon.c @@ -181,7 +181,7 @@ static void sigHandler(int sig) void cleanup(void) { remove("/tmp/percBat"); - display_free(); + display_close(); close(sar_fd); } @@ -442,6 +442,8 @@ float interpolatePercentage(float voltage) return 100; if (voltage <= VoltageCurveMapping_liion[table_size - 1].voltage) return 0; + + return -1; // Error } int batteryPercentage(int adcValue) @@ -458,6 +460,7 @@ int batteryPercentage(int adcValue) return (int)(adcValue * 2.125 - 1068); if (adcValue >= 480) return (int)(adcValue * 0.51613 - 243.742); + return 0; } // Convert ADC value to voltage diff --git a/src/common/system/display.h b/src/common/system/display.h index c012ed661..2803bae2b 100644 --- a/src/common/system/display.h +++ b/src/common/system/display.h @@ -14,20 +14,47 @@ #define display_on() display_setScreen(true) #define display_off() display_setScreen(false) -static uint32_t *fb_addr; static int fb_fd; -static uint8_t *fbofs; -static struct fb_fix_screeninfo finfo; -static struct fb_var_screeninfo vinfo; -static uint32_t stride, bpp; -static uint8_t *savebuf; -static bool display_enabled = true; static int DISPLAY_WIDTH = 640; // physical screen resolution static int DISPLAY_HEIGHT = 480; int RENDER_WIDTH = 640; // render resolution int RENDER_HEIGHT = 480; struct timeval start_time, end_time; +typedef struct Display { + long fb_size; + int width; + int height; + int bpp; + int stride; + uint32_t *fb_addr; + uint8_t *fb_ofs; + struct fb_fix_screeninfo finfo; + struct fb_var_screeninfo vinfo; + uint8_t *savebuf; + bool enabled; + bool init_done; +} display_t; + +static display_t g_display = { + .width = 640, + .height = 480, + .bpp = 32, + .stride = 0, + .fb_size = 0, + .fb_addr = NULL, + .fb_ofs = NULL, + .enabled = true, + .init_done = false, +}; + +typedef struct Rect { + int x; + int y; + int w; + int h; +} rect_t; + // // Get render resolution // @@ -36,9 +63,9 @@ void display_getRenderResolution() print_debug("Getting render resolution\n"); if (fb_fd < 0) fb_fd = open("/dev/fb0", O_RDWR); - ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo); - RENDER_WIDTH = vinfo.xres; - RENDER_HEIGHT = vinfo.yres; + ioctl(fb_fd, FBIOGET_VSCREENINFO, &g_display.vinfo); + RENDER_WIDTH = g_display.vinfo.xres; + RENDER_HEIGHT = g_display.vinfo.yres; } // @@ -58,13 +85,15 @@ void display_getResolution() void display_init(void) { + if (g_display.init_done) + return; // Open and mmap FB fb_fd = open("/dev/fb0", O_RDWR); - ioctl(fb_fd, FBIOGET_FSCREENINFO, &finfo); - fb_addr = (uint32_t *)mmap(0, finfo.smem_len, PROT_READ | PROT_WRITE, - MAP_SHARED, fb_fd, 0); + ioctl(fb_fd, FBIOGET_FSCREENINFO, &g_display.finfo); + g_display.fb_addr = (uint32_t *)mmap(0, g_display.finfo.smem_len, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0); display_getResolution(); display_getRenderResolution(); + g_display.init_done = true; } // @@ -72,19 +101,19 @@ void display_init(void) // void display_save(void) { - stride = finfo.line_length; - ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo); - bpp = vinfo.bits_per_pixel / 8; // byte per pixel - fbofs = (uint8_t *)fb_addr + (vinfo.yoffset * stride); + g_display.stride = g_display.finfo.line_length; + ioctl(fb_fd, FBIOGET_VSCREENINFO, &g_display.vinfo); + g_display.bpp = g_display.vinfo.bits_per_pixel / 8; // byte per pixel + g_display.fb_ofs = (uint8_t *)g_display.fb_addr + (g_display.vinfo.yoffset * g_display.stride); // Save display area and clear - if ((savebuf = (uint8_t *)malloc(DISPLAY_WIDTH * bpp * DISPLAY_HEIGHT))) { + if ((g_display.savebuf = (uint8_t *)malloc(DISPLAY_WIDTH * g_display.bpp * DISPLAY_HEIGHT))) { uint32_t i, ofss, ofsd; ofss = ofsd = 0; for (i = DISPLAY_HEIGHT; i > 0; - i--, ofss += stride, ofsd += DISPLAY_WIDTH * bpp) { - memcpy(savebuf + ofsd, fbofs + ofss, DISPLAY_WIDTH * bpp); - memset(fbofs + ofss, 0, DISPLAY_WIDTH * bpp); + i--, ofss += g_display.stride, ofsd += DISPLAY_WIDTH * g_display.bpp) { + memcpy(g_display.savebuf + ofsd, g_display.fb_ofs + ofss, DISPLAY_WIDTH * g_display.bpp); + memset(g_display.fb_ofs + ofss, 0, DISPLAY_WIDTH * g_display.bpp); } } } @@ -95,24 +124,36 @@ void display_save(void) void display_restore(void) { // Restore display area - if (savebuf) { + if (g_display.savebuf) { uint32_t i, ofss, ofsd; ofss = ofsd = 0; for (i = DISPLAY_HEIGHT; i > 0; - i--, ofsd += stride, ofss += DISPLAY_WIDTH * bpp) { - memcpy(fbofs + ofsd, savebuf + ofss, DISPLAY_WIDTH * bpp); + i--, ofsd += g_display.stride, ofss += DISPLAY_WIDTH * g_display.bpp) { + memcpy(g_display.fb_ofs + ofsd, g_display.savebuf + ofss, DISPLAY_WIDTH * g_display.bpp); } - free(savebuf); - savebuf = NULL; + free(g_display.savebuf); + g_display.savebuf = NULL; } } void display_reset(void) { - ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo); - vinfo.yoffset = 0; - memset(fb_addr, 0, finfo.smem_len); - ioctl(fb_fd, FBIOPUT_VSCREENINFO, &vinfo); + ioctl(fb_fd, FBIOGET_VSCREENINFO, &g_display.vinfo); + g_display.vinfo.yoffset = 0; + memset(g_display.fb_addr, 0, g_display.finfo.smem_len); + ioctl(fb_fd, FBIOPUT_VSCREENINFO, &g_display.vinfo); +} + +void display_free(display_t *display) +{ + if (display->savebuf) { + free(display->savebuf); + display->savebuf = NULL; + } + if (display->fb_addr) { + munmap(display->fb_addr, display->fb_size); + display->fb_addr = NULL; + } } // @@ -140,10 +181,10 @@ void display_setScreen(bool enabled) else { display_save(); } - display_enabled = enabled; + g_display.enabled = enabled; } -void display_toggle(void) { display_setScreen(!display_enabled); } +void display_toggle(void) { display_setScreen(!g_display.enabled); } uint32_t display_getBrightnessRaw() { @@ -185,12 +226,160 @@ void display_setBrightness(uint32_t value) display_setBrightnessRaw(value_raw); } +/** + * @brief Read from or write to a framebuffer buffer. + * + * This function reads from or writes to a specific buffer in the framebuffer. + * It supports optional rotation of the buffer content. + * + * @param index The index of the buffer to read/write. + * @param pixels Pointer to the pixel data. + * @param rect The rectangle area to read/write. + * @param rotate Whether to rotate the buffer content. + * @param write Whether to write to the buffer (true) or read from it (false). + * @param mask Whether to use a mask when writing to the buffer. + */ +void display_readOrWriteBuffer(int index, display_t *display, uint32_t *pixels, rect_t rect, bool rotate, bool mask, bool write) +{ + int bufferPos = index * display->vinfo.yres; + + for (int oy = 0; oy < rect.h; oy++) { + int y = rect.y + oy; + + if (y < 0 || y >= display->vinfo.yres) + continue; + + int virtualY = bufferPos + (rotate ? (display->vinfo.yres - 1) - y : y); + long baseOffset = (long)virtualY * display->vinfo.xres; + int baseIndex = oy * rect.w; + + for (int ox = 0; ox < rect.w; ox++) { + int x = rect.x + ox; + + if (rotate) { + x = (display->vinfo.xres - 1) - x; + } + + if (x < 0 || x >= display->vinfo.xres) + continue; + + long offset = baseOffset + (long)x; + int index = baseIndex + ox; + if (write) { + if (mask) { + if (pixels[index] != 0) { + display->fb_addr[offset] = 0; + } + } + else { + display->fb_addr[offset] = pixels[index]; + } + } + else { + if (mask) { + pixels[index] = display->fb_addr[offset] == 0 ? 1 : 0; + } + else { + pixels[index] = display->fb_addr[offset]; + } + } + } + } +} + +/** + * @brief Read from a framebuffer buffer. + * + * This function reads from a specific buffer in the framebuffer. + * It supports optional rotation of the buffer content. + * + * @param pixels Pointer to the pixel data. + * @param rect The rectangle area to read/write. + * @param bufferPos The starting position of the buffer. + * @param rotate Whether to rotate the buffer content. + * @param mask Whether to use a mask when writing to the buffer. + */ +void display_readBuffer(int bufferPos, display_t *display, uint32_t *pixels, rect_t rect, bool rotate, bool mask) +{ + display_readOrWriteBuffer(bufferPos, display, pixels, rect, rotate, mask, false); +} + +/** + * @brief Write to a framebuffer buffer. + * + * This function writes to a specific buffer in the framebuffer. + * It supports optional rotation of the buffer content. + * + * @param pixels Pointer to the pixel data. + * @param rect The rectangle area to read/write. + * @param bufferPos The starting position of the buffer. + * @param rotate Whether to rotate the buffer content. + * @param mask Whether to use a mask when writing to the buffer. + */ +void display_writeBuffer(int bufferPos, display_t *display, uint32_t *pixels, rect_t rect, bool rotate, bool mask) +{ + display_readOrWriteBuffer(bufferPos, display, pixels, rect, rotate, mask, true); +} + +/** + * @brief Read from or write to multiple framebuffer buffers. + * + * This function reads from or writes to multiple buffers in the framebuffer. + * It supports optional rotation of the buffer content. + * + * @param pixels Array of pointers to the pixel data for each buffer. + * @param rect The rectangle area to read/write. + * @param rotate Whether to rotate the buffer content. + * @param write Whether to write to the buffers (true) or read from them (false). + * @param mask Whether to use a mask when writing to the buffers. + */ +void display_readOrWriteBuffers(display_t *display, uint32_t **pixels, rect_t rect, bool rotate, bool mask, bool write) +{ + int numBuffers = display->vinfo.yres_virtual / display->vinfo.yres; + + for (int b = 0; b < numBuffers; b++) { + display_readOrWriteBuffer(b, display, pixels[b], rect, rotate, mask, write); + } +} + +/** + * @brief Read from multiple framebuffer buffers. + * + * This function reads from multiple buffers in the framebuffer. + * It supports optional rotation of the buffer content. + * + * @param pixels Array of pointers to the pixel data for each buffer. + * @param rect The rectangle area to read/write. + * @param rotate Whether to rotate the buffer content. + * @param mask Whether to use a mask when writing to the buffers. + */ +void display_readBuffers(display_t *display, uint32_t **pixels, rect_t rect, bool rotate, bool mask) +{ + display_readOrWriteBuffers(display, pixels, rect, rotate, mask, false); +} + +/** + * @brief Write to multiple framebuffer buffers. + * + * This function writes to multiple buffers in the framebuffer. + * It supports optional rotation of the buffer content. + * + * @param pixels Array of pointers to the pixel data for each buffer. + * @param rect The rectangle area to read/write. + * @param rotate Whether to rotate the buffer content. + * @param mask Whether to use a mask when writing to the buffers. + */ +void display_writeBuffers(display_t *display, uint32_t **pixels, rect_t rect, bool rotate, bool mask) +{ + display_readOrWriteBuffers(display, pixels, rect, rotate, mask, true); +} + // // Draw frame, fixed 640x480x32bpp for now // void display_drawFrame(uint32_t color) { - uint32_t *ofs = fb_addr; + uint32_t *ofs = g_display.fb_addr; uint32_t i; for (i = 0; i < 640; i++) { ofs[i] = color; @@ -207,7 +396,7 @@ void display_drawFrame(uint32_t color) for (i = 0; i < 640; i++) { ofs[i] = color; } - ofs = fb_addr + 639; + ofs = g_display.fb_addr + 639; for (i = 0; i < 480 * 3 - 1; i++, ofs += 640) { ofs[0] = color; ofs[1] = color; @@ -220,7 +409,7 @@ void display_drawFrame(uint32_t color) void display_drawBatteryIcon(uint32_t color, int x, int y, int level, uint32_t fillColor) { - uint32_t *ofs = fb_addr; + uint32_t *ofs = g_display.fb_addr; int i, j; // Draw battery body wireframe @@ -249,12 +438,9 @@ void display_drawBatteryIcon(uint32_t color, int x, int y, int level, } } -void display_free(void) +void display_close(void) { - if (savebuf) - free(savebuf); - if (fb_addr) - munmap(fb_addr, finfo.smem_len); + display_free(&g_display); if (fb_fd > 0) close(fb_fd); } diff --git a/src/common/system/osd.h b/src/common/system/osd.h index 4a0dc39fa..2ff0ee0d4 100644 --- a/src/common/system/osd.h +++ b/src/common/system/osd.h @@ -1,11 +1,13 @@ #ifndef SYSTEM_OSD_H__ #define SYSTEM_OSD_H__ +#include #include #include "utils/config.h" #include "utils/log.h" #include "utils/msleep.h" +#include "utils/timer.h" #include "./clock.h" #include "./display.h" @@ -23,109 +25,235 @@ #define OSD_VOLUME_COLOR OSD_COLOR_GREEN #define OSD_MUTE_ON_COLOR OSD_COLOR_RED +#ifndef CLOCK_MONOTONIC +#define CLOCK_MONOTONIC 1 +#endif + static bool osd_thread_active = false; static pthread_t osd_pt; -// -// Print digit -// -void print_digit(uint8_t num, uint32_t x, uint32_t color) +typedef struct { + SDL_Surface *surface; + int destX; + int destY; + int duration_ms; + bool rotate; + bool useMask; + display_t display; +} overlay_thread_data; + +typedef struct { + bool cancelled; + pthread_t thread_id; + pthread_mutex_t lock; +} task_t; + +static task_t g_overlay_task = {true, 0, PTHREAD_MUTEX_INITIALIZER}; + +/** + * @brief Cancels the overlay and cleans up resources. + * + * Called by overlay_surface() if there is an existing overlay, or manually by + * the user to cancel an overlay. (future use - gameswitcher :)) + */ +void cancel_overlay() { - const uint16_t pix[13] = {0b000000000000000, // space - 0b000001010100000, // / - 0b111101101101111, // 0 - 0b001001001001001, // 1 - 0b111001111100111, // 2 - 0b111001111001111, // 3 - 0b101101111001001, // 4 - 0b111100111001111, // 5 - 0b111100111101111, // 6 - 0b111001001001001, // 7 - 0b111101111101111, // 8 - 0b111101111001111, // 9 - 0b000010000010000}; // : - uint32_t c32, i, y; - uint16_t number_pixel, c16; - uint8_t *ofs; - uint16_t *ofs16; - uint32_t *ofs32; - uint32_t s16 = stride / 2; - uint32_t s32 = stride / 4; - - if (num == ' ') - num = 0; - else - num -= 0x2e; - if ((num > 12) || (x > 18)) + if (g_overlay_task.thread_id == 0) { + print_debug("No overlay to cancel\n"); return; - number_pixel = pix[num]; - ofs = fbofs + ((18 - x) * CHR_WIDTH * bpp); - - printf_debug("printing %d\n", num); - - for (y = 5; y > 0; y--, ofs += stride * 4) { - if (bpp == 4) { - ofs32 = (uint32_t *)ofs; - for (i = 3; i > 0; i--, number_pixel >>= 1, ofs32 += 4) { - c32 = (number_pixel & 1) ? color : 0; - ofs32[0] = c32; - ofs32[1] = c32; - ofs32[2] = c32; - ofs32[3] = c32; - ofs32[s32 + 0] = c32; - ofs32[s32 + 1] = c32; - ofs32[s32 + 2] = c32; - ofs32[s32 + 3] = c32; - ofs32[s32 * 2 + 0] = c32; - ofs32[s32 * 2 + 1] = c32; - ofs32[s32 * 2 + 2] = c32; - ofs32[s32 * 2 + 3] = c32; - ofs32[s32 * 3 + 0] = c32; - ofs32[s32 * 3 + 1] = c32; - ofs32[s32 * 3 + 2] = c32; - ofs32[s32 * 3 + 3] = c32; + } + // Mark abort + pthread_mutex_lock(&g_overlay_task.lock); + pthread_t thread_to_join = g_overlay_task.thread_id; + printf_debug("Cancelling overlay thread ID %ld\n", thread_to_join); + g_overlay_task.cancelled = true; + pthread_mutex_unlock(&g_overlay_task.lock); + + pthread_join(thread_to_join, NULL); + + printf_debug("Cancelled overlay thread ID %ld\n", thread_to_join); +} + +static void free_overlay_data(overlay_thread_data *data) +{ + if (data->surface) { + SDL_FreeSurface(data->surface); + data->surface = NULL; + } + display_free(&data->display); + free(data); +} + +static void *_overlay_draw_thread(void *arg) +{ + overlay_thread_data *data = (overlay_thread_data *)arg; + + // Backup original fb content + START_TIMER(framebuffer_backup); + int numBuffers = data->display.vinfo.yres_virtual / data->display.vinfo.yres; + unsigned int **originalPixels = malloc(numBuffers * sizeof(unsigned int *)); + if (!originalPixels) { + return NULL; + } + for (int b = 0; b < numBuffers; b++) { + originalPixels[b] = malloc(data->surface->w * data->surface->h * sizeof(unsigned int)); + if (!originalPixels[b]) { + for (int i = 0; i < b; i++) { + free(originalPixels[i]); } + free(originalPixels); + return NULL; } - else { - ofs16 = (uint16_t *)ofs; - for (i = 3; i > 0; i--, number_pixel >>= 1, ofs16 += 4) { - c16 = (number_pixel & 1) ? (color & 0xffff) : 0; - ofs16[0] = c16; - ofs16[1] = c16; - ofs16[2] = c16; - ofs16[3] = c16; - ofs16[s16 + 0] = c16; - ofs16[s16 + 1] = c16; - ofs16[s16 + 2] = c16; - ofs16[s16 + 3] = c16; - ofs16[s16 * 2 + 0] = c16; - ofs16[s16 * 2 + 1] = c16; - ofs16[s16 * 2 + 2] = c16; - ofs16[s16 * 2 + 3] = c16; - ofs16[s16 * 3 + 0] = c16; - ofs16[s16 * 3 + 1] = c16; - ofs16[s16 * 3 + 2] = c16; - ofs16[s16 * 3 + 3] = c16; - } + } + + rect_t rect = {data->destX, data->destY, data->surface->w, data->surface->h}; + + display_readBuffers(&data->display, originalPixels, rect, data->rotate, data->useMask); + END_TIMER(framebuffer_backup); + printf_debug("Backup buffer total size: %d KiB\n", + (numBuffers * data->surface->w * data->surface->h * sizeof(unsigned int)) / 1024); + + struct timespec start, now; + clock_gettime(CLOCK_MONOTONIC, &start); + long elapsed_ms = -1; + int draw_count = 0; + + // Continuously blit the overlay to each buffer + while (true) { + clock_gettime(CLOCK_MONOTONIC, &now); + elapsed_ms = + (long)(now.tv_sec - start.tv_sec) * 1000L + + (long)(now.tv_nsec - start.tv_nsec) / 1000000L; + + // Timeout + if (data->duration_ms > 0 && elapsed_ms >= data->duration_ms) + break; + + // Abort + pthread_mutex_lock(&g_overlay_task.lock); + bool local_abort = g_overlay_task.cancelled; + pthread_mutex_unlock(&g_overlay_task.lock); + if (local_abort) + break; + + // Draw to each buffer + numBuffers = data->display.vinfo.yres_virtual / data->display.vinfo.yres; + for (int b = 0; b < numBuffers; b++) { + draw_count++; + display_writeBuffer(b, &data->display, data->surface->pixels, rect, data->rotate, false); } + + // TODO: sleep or not? atm i'd say no + // usleep(4000); + } + + printf("Draw count: %d\n", draw_count); + printf("Draw speed: %f\n", (float)draw_count / (float)elapsed_ms * 1000.0f); + + // Restore original framebuffer content after overlay + // TODO: If the content "behind" the overlay has changed, this will not restore it correctly, causing a 1 frame glitch. How to fix? Only backup if the overlay is (partially) outside the game screen? + // TODO: Theoretically the backup/restore is only needed if the position of the overlay is outside the game screen because that part of the screen is not updated by the game. + + START_TIMER(framebuffer_restore); + display_writeBuffers(&data->display, originalPixels, rect, data->rotate, data->useMask); + + // Free buffer backups + numBuffers = data->display.vinfo.yres_virtual / data->display.vinfo.yres; + for (int b = 0; b < numBuffers; b++) { + free(originalPixels[b]); } + free(originalPixels); + END_TIMER(framebuffer_restore); + + free_overlay_data(data); + + // Clean up thread data + pthread_mutex_lock(&g_overlay_task.lock); + g_overlay_task.cancelled = true; + g_overlay_task.thread_id = 0; + pthread_mutex_unlock(&g_overlay_task.lock); + + pthread_exit(NULL); } -// -// Print number value -// -// Color: white=0x00FFFFFF, red=0x00FF0000 -// -void print_value(uint32_t value, uint32_t color) +/** + * @brief Overlays an SDL_Surface onto the framebuffer at a specified position and duration. + * + * Flickering is minimized by drawing to all buffers in the framebuffer. + * Should work regardless of the count of buffers (double, triple, etc. buffering). + * + * @param surface The SDL_Surface to overlay. The surface will be freed by this function. + * @param destX The X coordinate on the screen where the overlay should be placed. + * @param destY The Y coordinate on the screen where the overlay should be placed. + * @param duration_ms The duration in milliseconds for which the overlay should be displayed. 0 for indefinite. + * @param rotate Should the overlay be rotated 180 degrees? + * @return int Returns 0 on success, -1 on failure. + */ +int overlay_surface(SDL_Surface *surface, int destX, int destY, int duration_ms, bool rotate) { - char str[20]; - sprintf(str, "%d", value); + printf_debug("Overlaying surface at %d, %d, size %dx%d, duration %d\n, rotate %d\n", + destX, destY, surface->w, surface->h, duration_ms, rotate); + display_init(); - printf_debug("osd: %s\n", str); + // If there's an existing thread, abort it + if (g_overlay_task.thread_id != 0) { + print_debug("Cancelling existing overlay\n"); + cancel_overlay(); + } + + // Data for thread + overlay_thread_data *data = (overlay_thread_data *)calloc(1, sizeof(overlay_thread_data)); + if (!data) { + perror("Failed to allocate overlay_thread_data\n"); + return -1; + } + + display_t *display = &data->display; + display->finfo = g_display.finfo; + + // Query FB info + if (ioctl(fb_fd, FBIOGET_VSCREENINFO, &display->vinfo) == -1) { + perror("Error reading variable screen info"); + return -1; + } + + if (display->vinfo.bits_per_pixel != 32) { + perror("Only 32bpp is supported for now\n"); + return -1; + } + + data->surface = surface; + data->destX = destX; + data->destY = destY; + data->duration_ms = duration_ms; + data->rotate = rotate; + data->useMask = false; // TODO: Use mask dependent on running application + + // Need to map every time in case of buffer changes + display->fb_size = (long)g_display.finfo.line_length * (long)display->vinfo.yres_virtual; + display->fb_addr = mmap(NULL, display->fb_size, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0); + if (display->fb_addr == MAP_FAILED) { + perror("Failed to mmap framebuffer"); + free_overlay_data(data); + return -1; + } - for (uint32_t x = 0; x < 19; x++) { - print_digit(str[x], x, color); + // Create the thread + print_debug("Creating overlay drawing thread\n"); + pthread_mutex_lock(&g_overlay_task.lock); + g_overlay_task.cancelled = false; + int rc = pthread_create(&g_overlay_task.thread_id, NULL, _overlay_draw_thread, data); + if (rc != 0) { + fprintf(stderr, "Failed to create overlay drawing thread\n"); + free_overlay_data(data); + pthread_mutex_unlock(&g_overlay_task.lock); + return -1; } + + printf_debug("Thread ID %ld created\n", g_overlay_task.thread_id); + pthread_mutex_unlock(&g_overlay_task.lock); + + return 0; } static int meterWidth = 4; @@ -141,7 +269,7 @@ static uint32_t *_bar_savebuf; void _print_bar(void) { #ifdef PLATFORM_MIYOOMINI - uint32_t *ofs = fb_addr; + uint32_t *ofs = g_display.fb_addr; uint32_t i, j, curr, percentage = _bar_max > 0 ? _bar_value * RENDER_HEIGHT / _bar_max : 0; ofs += RENDER_WIDTH - meterWidth; @@ -161,7 +289,7 @@ void _bar_restoreBufferBehind(void) _bar_color = 0; _print_bar(); if (_bar_savebuf) { - uint32_t i, j, *ofs = fb_addr, *ofss = _bar_savebuf; + uint32_t i, j, *ofs = g_display.fb_addr, *ofss = _bar_savebuf; ofs += RENDER_WIDTH - meterWidth; ofss += RENDER_WIDTH - meterWidth; for (i = 0; i < RENDER_HEIGHT; i++, ofs += RENDER_WIDTH, ofss += RENDER_WIDTH) { @@ -180,7 +308,7 @@ void _bar_saveBufferBehind(void) // Save display area and clear if ((_bar_savebuf = (uint32_t *)malloc(RENDER_WIDTH * RENDER_HEIGHT * sizeof(uint32_t)))) { - uint32_t i, j, *ofs = fb_addr, *ofss = _bar_savebuf; + uint32_t i, j, *ofs = g_display.fb_addr, *ofss = _bar_savebuf; ofs += RENDER_WIDTH - meterWidth; ofss += RENDER_WIDTH - meterWidth; for (i = 0; i < RENDER_HEIGHT; i++, ofs += RENDER_WIDTH, ofss += RENDER_WIDTH) { @@ -198,7 +326,7 @@ static void *_osd_thread(void *_) { while (getMilliseconds() - _bar_timer < 2000) { _print_bar(); - msleep(1); + usleep(100); } _bar_restoreBufferBehind(); osd_thread_active = false; diff --git a/src/common/system/screenshot.h b/src/common/system/screenshot.h index 9ec903569..03b861767 100644 --- a/src/common/system/screenshot.h +++ b/src/common/system/screenshot.h @@ -91,8 +91,8 @@ uint32_t *__screenshot_buffer(void) size_t buffer_size = DISPLAY_WIDTH * DISPLAY_HEIGHT * sizeof(uint32_t); uint32_t *buffer = (uint32_t *)malloc(buffer_size); - ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo); - memcpy(buffer, fb_addr + DISPLAY_WIDTH * vinfo.yoffset, buffer_size); + ioctl(fb_fd, FBIOGET_VSCREENINFO, &g_display.vinfo); + memcpy(buffer, g_display.fb_addr + DISPLAY_WIDTH * g_display.vinfo.yoffset, buffer_size); return buffer; } diff --git a/src/common/utils/log.c b/src/common/utils/log.c index 087ca1e84..473ef2422 100644 --- a/src/common/utils/log.c +++ b/src/common/utils/log.c @@ -16,7 +16,7 @@ void log_setName(const char *log_name) void log_debug(const char *file_path, int line, const char *format_str, ...) { - char log_message[1024], cmd[1024]; + char log_message[1024]; va_list valist; va_start(valist, format_str); @@ -24,10 +24,7 @@ void log_debug(const char *file_path, int line, const char *format_str, ...) vsprintf(log_message + strlen(log_message), format_str, valist); va_end(valist); - char *no_underscore = str_replace(log_message, "\"", "\\\""); - snprintf(cmd, 1023, "echo -n \"%s\" >/dev/stderr", no_underscore); - system(cmd); - free(no_underscore); + fprintf(stderr, "%s", log_message); if (strlen(_log_path) == 0) return; diff --git a/src/common/utils/process.h b/src/common/utils/process.h index 55f830930..bc0c145ca 100644 --- a/src/common/utils/process.h +++ b/src/common/utils/process.h @@ -92,4 +92,31 @@ bool process_start(const char *pname, const char *args, const char *home, return true; } +bool process_start_read_return(const char *cmdline, char *out_str) +{ + char buffer[255] = ""; + char *result = NULL; + + FILE *pipe = popen(cmdline, "r"); + if (pipe == NULL) { + fprintf(stderr, "Error executing command: %s\n", cmdline); + return -1; + } + + while (fgets(buffer, sizeof(buffer), pipe) != NULL) { + result = strdup(buffer); + } + + pclose(pipe); + if (result != NULL) { + result[strlen(buffer) - 1] = '\0'; + strcpy(out_str, result); + free(result); + } + else { + strcpy(out_str, ""); + } + return 0; +} + #endif // PROCESS_H__ diff --git a/src/common/utils/timer.h b/src/common/utils/timer.h new file mode 100644 index 000000000..432a659a9 --- /dev/null +++ b/src/common/utils/timer.h @@ -0,0 +1,34 @@ +#ifndef TIMER_H__ +#define TIMER_H__ + +#include +#include + +#define START_TIMER(struct_name) \ + struct timeval struct_name##_before, struct_name##_after, struct_name##_result; \ + gettimeofday(&struct_name##_before, NULL); + +#define END_TIMER(struct_name) \ + gettimeofday(&struct_name##_after, NULL); \ + timersub(&struct_name##_after, &struct_name##_before, &struct_name##_result); \ + long struct_name##_milliseconds = struct_name##_result.tv_sec * 1000 + struct_name##_result.tv_usec / 1000; \ + printf("\033[1;33m%s: %ld milliseconds\033[0m\n", #struct_name, struct_name##_milliseconds); + +#endif + +/* + * Usage: + * + * #include "timer.h" + * + * int main() { + * START_TIMER(loading); + * some_loading_function(); + * END_TIMER(loading); + * return 0; + * } + * + * Output (stdout): + * + * loading: 123 milliseconds + */ diff --git a/src/cpuclock/Makefile b/src/cpuclock/Makefile new file mode 100644 index 000000000..208e2589b --- /dev/null +++ b/src/cpuclock/Makefile @@ -0,0 +1,8 @@ +include ../common/config.mk + +TARGET = cpuclock +CFLAGS := $(CFLAGS) +LDFLAGS := $(LDFLAGS) + +include ../common/commands.mk +include ../common/recipes.mk diff --git a/src/cpuclock/cpuclock.c b/src/cpuclock/cpuclock.c new file mode 100644 index 000000000..df5676bb6 --- /dev/null +++ b/src/cpuclock/cpuclock.c @@ -0,0 +1,145 @@ +// +// miyoomini over/underclocking tool +// +#include +#include +#include +#include +#include +#include +#include + +#include "system/device_model.h" + +#define BASE_REG_RIU_PA (0x1F000000) +#define BASE_REG_MPLL_PA (BASE_REG_RIU_PA + 0x103000 * 2) +#define PLL_SIZE (0x1000) + +void *pll_map; + +void print_clock(void) +{ + uint32_t rate; + uint32_t lpf_value; + uint32_t post_div; + volatile uint8_t *p8 = (uint8_t *)pll_map; + volatile uint16_t *p16 = (uint16_t *)pll_map; + + //get LPF / post_div + lpf_value = p16[0x2A4] + (p16[0x2A6] << 16); + post_div = p16[0x232] + 1; + if (lpf_value == 0) + lpf_value = (p8[0x2C2 << 1] << 16) + (p8[0x2C1 << 1] << 8) + p8[0x2C0 << 1]; + + /* + * Calculate LPF value for DFS + * LPF_value(5.19) = (432MHz / Ref_clk) * 2^19 => it's for post_div=2 + * Ref_clk = CPU_CLK * 2 / 32 + */ + static const uint64_t divsrc = 432000000llu * 524288; + rate = (divsrc / lpf_value * 2 / post_div * 16); + + // print MHz + printf("%d\n", rate / 1000000); +} + +void writefile(const char *fname, char *str) +{ + int fd = open(fname, O_WRONLY); + if (fd >= 0) { + write(fd, str, strlen(str)); + close(fd); + } +} + +// +// set cpu clock +// set governor = userspace, clk = 1200000 before call +// +void set_cpuclock(int clock) +{ + uint32_t post_div; + if (clock >= 800000) + post_div = 2; + else if (clock >= 400000) + post_div = 4; + else if (clock >= 200000) + post_div = 8; + else + post_div = 16; + + static const uint64_t divsrc = 432000000llu * 524288; + uint32_t rate = (clock * 1000) / 16 * post_div / 2; + uint32_t lpf = (uint32_t)(divsrc / rate); + volatile uint16_t *p16 = (uint16_t *)pll_map; + + uint32_t cur_post_div = (p16[0x232] & 0x0F) + 1; + uint32_t tmp_post_div = cur_post_div; + if (post_div > cur_post_div) { + while (tmp_post_div != post_div) { + tmp_post_div <<= 1; + p16[0x232] = (p16[0x232] & 0xF0) | ((tmp_post_div - 1) & 0x0F); + } + } + + p16[0x2A8] = 0x0000; //reg_lpf_enable = 0 + p16[0x2AE] = 0x000F; //reg_lpf_update_cnt = 32 + p16[0x2A4] = lpf & 0xFFFF; //set target freq to LPF high + p16[0x2A6] = lpf >> 16; //set target freq to LPF high + p16[0x2B0] = 0x0001; //switch to LPF control + p16[0x2B2] |= 0x1000; //from low to high + p16[0x2A8] = 0x0001; //reg_lpf_enable = 1 + while (!(p16[0x2BA] & 1)) + ; //polling done + p16[0x2A0] = lpf & 0xFFFF; //store freq to LPF low + p16[0x2A2] = lpf >> 16; //store freq to LPF low + + if (post_div < cur_post_div) { + while (tmp_post_div != post_div) { + tmp_post_div >>= 1; + p16[0x232] = (p16[0x232] & 0xF0) | ((tmp_post_div - 1) & 0x0F); + } + } +} + +int main(int argc, char *argv[]) +{ + getDeviceModel(); + if (DEVICE_ID != MIYOO354 && DEVICE_ID != MIYOO283) { + puts("This tool is only for Miyoo Mini"); + return 1; + } + + int fd_mem = open("/dev/mem", O_RDWR); + pll_map = mmap(0, PLL_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd_mem, BASE_REG_MPLL_PA); + + int clock = 0; + if (argc == 1) { + print_clock(); + return 0; + } + if (argc == 2) + clock = atoi(argv[1]); + + if ((clock < 100) || (clock > 2400)) { + puts("usage: cpuclock freq[MHz, 100 - 2400]"); + munmap(pll_map, PLL_SIZE); + close(fd_mem); + return 1; + } + + const char fn_governor[] = "/sys/devices/system/cpu/cpufreq/policy0/scaling_governor"; + const char fn_setspeed[] = "/sys/devices/system/cpu/cpufreq/policy0/scaling_setspeed"; + char clockstr[16]; + sprintf(clockstr, "%d", clock * 1000); + writefile(fn_governor, "userspace"); + writefile(fn_setspeed, clockstr); + + set_cpuclock(clock * 1000); + print_clock(); + + munmap(pll_map, PLL_SIZE); + close(fd_mem); + + return 0; +} diff --git a/src/keymon/Makefile b/src/keymon/Makefile index c634e70ca..703ddbf81 100644 --- a/src/keymon/Makefile +++ b/src/keymon/Makefile @@ -8,7 +8,7 @@ CFILES := $(CFILES) \ TARGET = keymon CFLAGS := $(CFLAGS) -Os -ffunction-sections -fdata-sections -LDFLAGS := $(LDFLAGS) -lpthread -lpng -Wl,--gc-sections +LDFLAGS := $(LDFLAGS) -lpthread -lpng -Wl,--gc-sections -lSDL -lSDL_ttf include ../common/commands.mk include ../common/recipes.mk diff --git a/src/keymon/keymon.c b/src/keymon/keymon.c index 331f88af2..db5ed4ec7 100644 --- a/src/keymon/keymon.c +++ b/src/keymon/keymon.c @@ -1,3 +1,5 @@ +#include +#include #include #include #include @@ -24,6 +26,8 @@ #include "system/system.h" #include "system/system_utils.h" #include "system/volume.h" +#include "theme/config.h" +#include "theme/resources.h" #include "utils/config.h" #include "utils/file.h" #include "utils/flags.h" @@ -163,7 +167,7 @@ void resume(void) // void quit(int exitcode) { - display_free(); + display_close(); if (input_fd > 0) close(input_fd); system_clock_get(); @@ -335,6 +339,109 @@ void turnOffScreen(void) suspend_exec(stay_awake ? -1 : timeout); } +/** + * @brief Creates an SDL_Surface containing text with a solid background color + * + * @param text The text to be rendered. + * @param fg The foreground color (text color). + * @param bg The background color. + * @param margin The margin around the text relative to background. + * @return SDL_Surface* The created surface containing the rendered text with margin. + * Returns NULL if the text rendering fails. + */ +SDL_Surface *createTextSurface(const char *text, SDL_Color fg, SDL_Color bg, int margin) +{ + TTF_Init(); + char theme_path[STR_MAX]; + theme_getPath(theme_path); + TTF_Font *font = theme_loadFont(theme_path, theme()->hint.font, 14); + + SDL_Surface *text_surface = TTF_RenderUTF8(font, text, fg, bg); + if (!text_surface) { + fprintf(stderr, "TTF_RenderUTF8 failed: %s\n", TTF_GetError()); + return 0; + } + int overlayW = text_surface->w + margin; + int overlayH = text_surface->h + margin; + + SDL_Surface *overlay_surface = SDL_CreateRGBSurface( + SDL_SWSURFACE, overlayW, overlayH, 32, + 0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000); + SDL_Rect dest; + dest.x = margin / 2; + dest.y = margin / 2; + dest.w = 0; + dest.h = 0; + SDL_BlitSurface(text_surface, NULL, overlay_surface, &dest); + TTF_CloseFont(font); + + SDL_FreeSurface(text_surface); + return overlay_surface; +} + +/** + * @brief Adjusts the CPU clock speed based on the given adjustment value and triggers an OSD message + * + * This function reads the current CPU clock speed, adjusts it by the specified + * amount, and sets the new CPU clock speed if it falls within the allowed range. + * It then triggers an OSD message to display the new CPU clock speed. + * If the new CPU clock speed is outside the allowed range, a short pulse is triggered + * and the function returns without making any changes. + * + * @param adjust The amount to adjust the CPU clock speed by (in MHz). + * + * @return void + */ +void cpuClockHotkey(int adjust) +{ + printf_debug("cpuClockHotkey: %d\n", adjust); + int min_cpu_clock = 500; // ? + int max_cpu_clock; + switch (DEVICE_ID) { + case MIYOO354: + max_cpu_clock = 1900; + break; + case MIYOO283: + max_cpu_clock = 1700; + break; + default: + // Unknown device + return; + } + char cpuclockstr[5]; + + // Read current CPU clock + int ret = process_start_read_return("cpuclock", cpuclockstr); + int cpuclock = atoi(cpuclockstr); + printf_debug("Current CPU clock: %d\n", cpuclock); + cpuclock += adjust; + printf_debug("Desired CPU clock: %d\n", cpuclock); + // Bounds check + if (cpuclock < min_cpu_clock || cpuclock > max_cpu_clock) { + printf_debug("Desired CPU clock %d out of range (%d, %d)\n", cpuclock, min_cpu_clock, max_cpu_clock); + SDL_Surface *surface = createTextSurface("CPU clock out of range", (SDL_Color){255, 255, 255, 255}, (SDL_Color){0, 0, 0, 0}, 10); + if (surface && overlay_surface(surface, 10, 10, 1000, true) != 0) { + SDL_FreeSurface(surface); + } + short_pulse(); + return; + } + + // Set new CPU clock + char cmd[STR_MAX]; + snprintf(cmd, STR_MAX, "cpuclock %d", cpuclock); + ret = process_start_read_return(cmd, cpuclockstr); + if (ret == 0) { + printf_debug("Updated CPU clock: %s\n", cpuclockstr); + char osd_txt[STR_MAX]; + snprintf(osd_txt, STR_MAX, "CPU clock set to %s MHz", cpuclockstr); + SDL_Surface *surface = createTextSurface(osd_txt, (SDL_Color){255, 255, 255, 255}, (SDL_Color){0, 0, 0, 0}, 10); + if (surface && overlay_surface(surface, 10, 10, 1000, true) != 0) { + SDL_FreeSurface(surface); + } + } +} + // // Main // @@ -506,6 +613,16 @@ int main(void) if (val != REPEAT) button_flag = (button_flag & (~START)) | (val << START_BIT); break; + case HW_BTN_R1: + if (val == PRESSED && (button_flag & (SELECT | START)) == (SELECT | START)) { + cpuClockHotkey(100); + } + break; + case HW_BTN_L1: + if (val == PRESSED && (button_flag & (SELECT | START)) == (SELECT | START)) { + cpuClockHotkey(-100); + } + break; case HW_BTN_L2: if (val == REPEAT) { // Adjust repeat speed to 1/2 diff --git a/src/tweaks/tweaks.c b/src/tweaks/tweaks.c index d31ca42fb..8f17033cd 100644 --- a/src/tweaks/tweaks.c +++ b/src/tweaks/tweaks.c @@ -104,7 +104,6 @@ int main(int argc, char *argv[]) bool key_changed = false; SDLKey changed_key = SDLK_UNKNOWN; - bool show_help_tooltip = !config_flag_get(".tweaksHelpCompleted"); while (!quit) { @@ -283,7 +282,7 @@ int main(int argc, char *argv[]) network_freeSmbShares(); diags_freeEntries(); - display_free(); + display_close(); lang_free(); menu_free_all(); diff --git a/static/build/.tmp_update/bin/cpuclock b/static/build/.tmp_update/bin/cpuclock deleted file mode 100644 index a4140f883b6e91ecc68ee89066bfcb2b302a59b8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18068 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 W)k7RUgmMnDgf$7g5hqWe@csvkG{I*8