diff --git a/src/dusk/console/console.c b/src/dusk/console/console.c index d712a48e..fe6e6119 100644 --- a/src/dusk/console/console.c +++ b/src/dusk/console/console.c @@ -12,9 +12,6 @@ #include "input/input.h" #include "log/log.h" #include "engine/engine.h" -#include "display/shader/shaderunlit.h" -#include "display/text/text.h" -#include "display/spritebatch/spritebatch.h" console_t CONSOLE; @@ -63,20 +60,6 @@ void consoleUpdate(void) { } } -errorret_t consoleDraw(void) { - if(!CONSOLE.visible) errorOk(); - - for(uint32_t i = 0; i < CONSOLE_HISTORY_MAX; i++) { - errorChain(textDraw( - 0, FONT_DEFAULT.tileset->tileHeight * i, - CONSOLE.line[i], - COLOR_RED, - &FONT_DEFAULT - )); - } - return spriteBatchFlush(); -} - void consoleDispose(void) { #ifdef DUSK_CONSOLE_POSIX threadMutexDispose(&CONSOLE.printMutex); diff --git a/src/dusk/console/console.h b/src/dusk/console/console.h index 309714b4..7ac6378b 100644 --- a/src/dusk/console/console.h +++ b/src/dusk/console/console.h @@ -45,13 +45,6 @@ void consolePrint(const char_t *message, ...); */ void consoleUpdate(void); -/** - * Renders the console history to the screen (UI space). - * - * @return The error return value. - */ -errorret_t consoleDraw(void); - /** * Disposes of the console. */ diff --git a/src/dusk/display/screen/screen.c b/src/dusk/display/screen/screen.c index 9e2e7d10..79920a12 100644 --- a/src/dusk/display/screen/screen.c +++ b/src/dusk/display/screen/screen.c @@ -13,6 +13,13 @@ screen_t SCREEN; +const screencropaspectinfo_t SCREEN_CROP_ASPECTS[SCREEN_CROP_ASPECT_COUNT] = { + [SCREEN_CROP_ASPECT_4_3] = { .ratio = 4.0f / 3.0f }, + [SCREEN_CROP_ASPECT_3_2] = { .ratio = 3.0f / 2.0f }, + [SCREEN_CROP_ASPECT_16_10] = { .ratio = 16.0f / 10.0f }, + [SCREEN_CROP_ASPECT_16_9] = { .ratio = 16.0f / 9.0f }, +}; + errorret_t screenInit() { memoryZero(&SCREEN, sizeof(screen_t)); @@ -53,10 +60,7 @@ errorret_t screenBind() { SCREEN.width = frameBufferGetWidth(FRAMEBUFFER_BOUND); SCREEN.height = frameBufferGetHeight(FRAMEBUFFER_BOUND); SCREEN.aspect = frameBufferGetAspect(FRAMEBUFFER_BOUND); - SCREEN.scanX = 0; - SCREEN.scanY = 0; - SCREEN.scanWidth = SCREEN.width; - SCREEN.scanHeight = SCREEN.height; + screenUpdateScan(); // No needd for a framebuffer. #ifdef DUSK_DISPLAY_SIZE_DYNAMIC @@ -73,10 +77,7 @@ errorret_t screenBind() { SCREEN.width = SCREEN.fixedSize.width; SCREEN.height = SCREEN.fixedSize.height; SCREEN.aspect = (float_t)SCREEN.width / (float_t)SCREEN.height; - SCREEN.scanX = 0; - SCREEN.scanY = 0; - SCREEN.scanWidth = SCREEN.width; - SCREEN.scanHeight = SCREEN.height; + screenUpdateScan(); if(SCREEN.framebufferReady) { // Is current framebuffer the correct size? @@ -114,10 +115,7 @@ errorret_t screenBind() { SCREEN.width = fbWidth; SCREEN.height = fbHeight; SCREEN.aspect = (float_t)SCREEN.width / (float_t)SCREEN.height; - SCREEN.scanX = 0; - SCREEN.scanY = 0; - SCREEN.scanWidth = SCREEN.width; - SCREEN.scanHeight = SCREEN.height; + screenUpdateScan(); if(SCREEN.framebufferReady) { errorChain(frameBufferDispose(&SCREEN.framebuffer)); @@ -148,10 +146,7 @@ errorret_t screenBind() { SCREEN.width = newFbWidth; SCREEN.height = newFbHeight; SCREEN.aspect = curFbAspect; - SCREEN.scanX = 0; - SCREEN.scanY = 0; - SCREEN.scanWidth = SCREEN.width; - SCREEN.scanHeight = SCREEN.height; + screenUpdateScan(); errorChain(frameBufferBind(&SCREEN.framebuffer)); errorOk(); } @@ -168,10 +163,7 @@ errorret_t screenBind() { SCREEN.width = newFbWidth; SCREEN.height = newFbHeight; SCREEN.aspect = (float_t)SCREEN.width / (float_t)SCREEN.height; - SCREEN.scanX = 0; - SCREEN.scanY = 0; - SCREEN.scanWidth = SCREEN.width; - SCREEN.scanHeight = SCREEN.height; + screenUpdateScan(); SCREEN.framebufferReady = true; // Bind FB @@ -191,10 +183,7 @@ errorret_t screenBind() { SCREEN.width = newFbWidth; SCREEN.height = newFbHeight; SCREEN.aspect = (float_t)SCREEN.width / (float_t)SCREEN.height; - SCREEN.scanX = 0; - SCREEN.scanY = 0; - SCREEN.scanWidth = SCREEN.width; - SCREEN.scanHeight = SCREEN.height; + screenUpdateScan(); if(fbWidth == newFbWidth && fbHeight == newFbHeight) { // No need to use framebuffer. @@ -241,10 +230,7 @@ errorret_t screenBind() { SCREEN.width = newFbWidth; SCREEN.height = newFbHeight; SCREEN.aspect = (float_t)SCREEN.width / (float_t)SCREEN.height; - SCREEN.scanX = 0; - SCREEN.scanY = 0; - SCREEN.scanWidth = SCREEN.width; - SCREEN.scanHeight = SCREEN.height; + screenUpdateScan(); if(fbWidth == newFbWidth && fbHeight == newFbHeight) { // No need to use framebuffer. @@ -286,10 +272,7 @@ errorret_t screenBind() { float_t fbAspect = fbWidth / fbHeight; SCREEN.width = (int32_t)floorf(SCREEN.height * fbAspect); SCREEN.aspect = (float_t)SCREEN.width / (float_t)SCREEN.height; - SCREEN.scanX = 0; - SCREEN.scanY = 0; - SCREEN.scanWidth = SCREEN.width; - SCREEN.scanHeight = SCREEN.height; + screenUpdateScan(); break; } @@ -423,6 +406,46 @@ errorret_t screenRender() { errorThrow("Invalid screen mode."); } +void screenUpdateScan(void) { + SCREEN.scanX = 0; + SCREEN.scanY = 0; + SCREEN.scanWidth = SCREEN.width; + SCREEN.scanHeight = SCREEN.height; + + #ifdef DUSK_DISPLAY_SIZE_DYNAMIC + // Find the smallest standard ratio >= the screen aspect. + float_t targetRatio = -1.0f; + for(int32_t i = 0; i < SCREEN_CROP_ASPECT_COUNT; i++) { + if(SCREEN_CROP_ASPECTS[i].ratio >= SCREEN.aspect) { + targetRatio = SCREEN_CROP_ASPECTS[i].ratio; + break; + } + } + if(targetRatio < 0.0f) { + // Wider than all entries: cap at last (16:9). + targetRatio = SCREEN_CROP_ASPECTS[ + SCREEN_CROP_ASPECT_COUNT - 1 + ].ratio; + } + + if(targetRatio > SCREEN.aspect) { + // Letterbox: target is wider, restrict height. + int32_t targetH = (int32_t)floorf( + (float_t)SCREEN.width / targetRatio + ); + SCREEN.scanY = (SCREEN.height - targetH) / 2; + SCREEN.scanHeight = targetH; + } else if(targetRatio < SCREEN.aspect) { + // Pillarbox: target is narrower, restrict width. + int32_t targetW = (int32_t)floorf( + (float_t)SCREEN.height * targetRatio + ); + SCREEN.scanX = (SCREEN.width - targetW) / 2; + SCREEN.scanWidth = targetW; + } + #endif +} + errorret_t screenDispose() { #ifdef DUSK_DISPLAY_SIZE_DYNAMIC if(SCREEN.framebufferReady) { diff --git a/src/dusk/display/screen/screen.h b/src/dusk/display/screen/screen.h index bf3a26a6..1213354f 100644 --- a/src/dusk/display/screen/screen.h +++ b/src/dusk/display/screen/screen.h @@ -17,6 +17,27 @@ #endif #endif +typedef enum { + SCREEN_CROP_ASPECT_4_3, + SCREEN_CROP_ASPECT_3_2, + SCREEN_CROP_ASPECT_16_10, + SCREEN_CROP_ASPECT_16_9, + SCREEN_CROP_ASPECT_COUNT +} screencropaspect_t; + +typedef struct { + float_t ratio; +} screencropaspectinfo_t; + +/** + * Standard crop aspect ratios, sorted narrowest to widest. + * screenUpdateScan() selects the smallest ratio >= the current + * screen aspect; screens wider than the widest entry are capped + * at the last entry (16:9). + */ +extern const screencropaspectinfo_t + SCREEN_CROP_ASPECTS[SCREEN_CROP_ASPECT_COUNT]; + typedef enum { SCREEN_MODE_BACKBUFFER, @@ -116,9 +137,20 @@ errorret_t screenUnbind(); */ errorret_t screenRender(); +/** + * Updates the scan area for the current screen dimensions. + * Defaults to the full viewport. On dynamic displays, finds the + * smallest SCREEN_CROP_ASPECTS entry whose ratio is >= the + * screen aspect and centers a crop window of that ratio. + * Screens narrower than the target are letterboxed; screens + * wider than the target are pillarboxed. Screens wider than + * all entries are pillarboxed to the last entry (16:9). + */ +void screenUpdateScan(void); + /** * Disposes the screen system. - * + * * @return Error code and state, if error occurs. */ errorret_t screenDispose(); \ No newline at end of file diff --git a/src/dusk/ui/CMakeLists.txt b/src/dusk/ui/CMakeLists.txt index ce5eae43..73801f42 100644 --- a/src/dusk/ui/CMakeLists.txt +++ b/src/dusk/ui/CMakeLists.txt @@ -7,6 +7,8 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC ui.c + uiconsole.c + uicrop.c uifps.c uielement.c uiframe.c diff --git a/src/dusk/ui/ui.c b/src/dusk/ui/ui.c index 6310ee13..182e9e4a 100644 --- a/src/dusk/ui/ui.c +++ b/src/dusk/ui/ui.c @@ -13,6 +13,7 @@ #include "ui/uielement.h" #include "ui/uifullbox.h" #include "ui/uiloading.h" +#include "ui/uicrop.h" #include "time/time.h" #include "log/log.h" @@ -20,6 +21,7 @@ ui_t UI; errorret_t uiInit(void) { memoryZero(&UI, sizeof(ui_t)); + uiCropInit(); uiFullboxInit(&UI_FULLBOX_UNDER); uiFullboxInit(&UI_FULLBOX_OVER); uiLoadingInit(); diff --git a/src/dusk/ui/uiconsole.c b/src/dusk/ui/uiconsole.c new file mode 100644 index 00000000..549cf000 --- /dev/null +++ b/src/dusk/ui/uiconsole.c @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uiconsole.h" +#include "console/console.h" +#include "display/screen/screen.h" +#include "display/text/text.h" +#include "display/spritebatch/spritebatch.h" + +errorret_t uiConsoleDraw(void) { + if(!CONSOLE.visible) errorOk(); + + float_t lineH = (float_t)FONT_DEFAULT.tileset->tileHeight; + for(uint32_t i = 0; i < CONSOLE_HISTORY_MAX; i++) { + errorChain(textDraw( + (float_t)SCREEN.scanX, + (float_t)SCREEN.scanY + lineH * (float_t)i, + CONSOLE.line[i], + COLOR_RED, + &FONT_DEFAULT + )); + } + return spriteBatchFlush(); +} diff --git a/src/dusk/ui/uiconsole.h b/src/dusk/ui/uiconsole.h new file mode 100644 index 00000000..2a227f74 --- /dev/null +++ b/src/dusk/ui/uiconsole.h @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" + +/** + * Renders the console history into the scan-safe area. + * No-ops when the console is not visible. + * + * @return Any error that occurs. + */ +errorret_t uiConsoleDraw(void); diff --git a/src/dusk/ui/uicrop.c b/src/dusk/ui/uicrop.c new file mode 100644 index 00000000..c3a2f016 --- /dev/null +++ b/src/dusk/ui/uicrop.c @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#include "uicrop.h" +#include "display/screen/screen.h" +#include "display/spritebatch/spritebatch.h" +#include "display/shader/shaderunlit.h" +#include "display/texture/texture.h" + +uicrop_t UI_CROP; + +void uiCropInit(void) { + UI_CROP.color = COLOR_BLACK; +} + +errorret_t uiCropDraw(void) { + if( + SCREEN.scanX == 0 && + SCREEN.scanY == 0 && + SCREEN.scanWidth == SCREEN.width && + SCREEN.scanHeight == SCREEN.height + ) errorOk(); + + float_t x0 = (float_t)SCREEN.scanX; + float_t y0 = (float_t)SCREEN.scanY; + float_t x1 = (float_t)(SCREEN.scanX + SCREEN.scanWidth); + float_t y1 = (float_t)(SCREEN.scanY + SCREEN.scanHeight); + float_t w = (float_t)SCREEN.width; + float_t h = (float_t)SCREEN.height; + + spritebatchsprite_t sprites[4]; + int32_t count = 0; + + if(SCREEN.scanX > 0) { + sprites[count++] = (spritebatchsprite_t){ + .min = { 0.0f, 0.0f, 0.0f }, + .max = { x0, h, 0.0f }, + .uvMin = { 0.0f, 0.0f }, + .uvMax = { 1.0f, 1.0f } + }; + } + + if(SCREEN.scanX + SCREEN.scanWidth < SCREEN.width) { + sprites[count++] = (spritebatchsprite_t){ + .min = { x1, 0.0f, 0.0f }, + .max = { w, h, 0.0f }, + .uvMin = { 0.0f, 0.0f }, + .uvMax = { 1.0f, 1.0f } + }; + } + + if(SCREEN.scanY > 0) { + sprites[count++] = (spritebatchsprite_t){ + .min = { x0, 0.0f, 0.0f }, + .max = { x1, y0, 0.0f }, + .uvMin = { 0.0f, 0.0f }, + .uvMax = { 1.0f, 1.0f } + }; + } + + if(SCREEN.scanY + SCREEN.scanHeight < SCREEN.height) { + sprites[count++] = (spritebatchsprite_t){ + .min = { x0, y1, 0.0f }, + .max = { x1, h, 0.0f }, + .uvMin = { 0.0f, 0.0f }, + .uvMax = { 1.0f, 1.0f } + }; + } + + if(count == 0) errorOk(); + + shadermaterial_t material = { + .unlit = { + .color = UI_CROP.color, + .texture = &TEXTURE_WHITE + } + }; + errorChain(spriteBatchBuffer(sprites, count, &SHADER_UNLIT, material)); + return spriteBatchFlush(); +} diff --git a/src/dusk/ui/uicrop.h b/src/dusk/ui/uicrop.h new file mode 100644 index 00000000..c0458583 --- /dev/null +++ b/src/dusk/ui/uicrop.h @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2026 Dominic Masters + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +#pragma once +#include "error/error.h" +#include "display/color.h" + +typedef struct { + color_t color; +} uicrop_t; + +extern uicrop_t UI_CROP; + +/** + * Initializes the crop bars to opaque black. + */ +void uiCropInit(void); + +/** + * Renders solid-color bars covering every area outside the + * current scan-safe region. No-ops when the scan area equals + * the full viewport. + * + * @return Any error that occurs. + */ +errorret_t uiCropDraw(void); diff --git a/src/dusk/ui/uielement.c b/src/dusk/ui/uielement.c index 7e8b8415..2849e4ff 100644 --- a/src/dusk/ui/uielement.c +++ b/src/dusk/ui/uielement.c @@ -7,15 +7,19 @@ #include "uielement.h" #include "assert/assert.h" -#include "console/console.h" #include "ui/uifps.h" #include "engine/engine.h" #include "ui/uitextbox.h" #include "ui/uifullbox.h" #include "ui/uiloading.h" #include "ui/uiplayerpos.h" +#include "ui/uicrop.h" +#include "ui/uiconsole.h" uielement_t UI_ELEMENTS[] = { + // Crop bars: black bars outside the scan-safe area. + { .type = UI_ELEMENT_TYPE_NATIVE, .draw = uiCropDraw }, + // Fullbox under: above scene, below system UI. { .type = UI_ELEMENT_TYPE_NATIVE, .draw = uiFullboxUnderDraw }, @@ -27,7 +31,7 @@ uielement_t UI_ELEMENTS[] = { // These render above the fullbox overlay. - { .type = UI_ELEMENT_TYPE_NATIVE, .draw = consoleDraw }, + { .type = UI_ELEMENT_TYPE_NATIVE, .draw = uiConsoleDraw }, { .type = UI_ELEMENT_TYPE_NATIVE, .draw = uiFPSDraw }, { .type = UI_ELEMENT_TYPE_NATIVE, .draw = uiPlayerPosDraw }, { .type = UI_ELEMENT_TYPE_NATIVE, .draw = uiLoadingDraw }, diff --git a/src/dusk/ui/uiplayerpos.c b/src/dusk/ui/uiplayerpos.c index dd6a17a5..29cc8a75 100644 --- a/src/dusk/ui/uiplayerpos.c +++ b/src/dusk/ui/uiplayerpos.c @@ -6,6 +6,7 @@ */ #include "uiplayerpos.h" +#include "display/screen/screen.h" #include "display/text/text.h" #include "display/spritebatch/spritebatch.h" #include "rpg/entity/entity.h" @@ -38,7 +39,10 @@ errorret_t uiPlayerPosDraw() { (int_t)chunkPos.z ); - float_t y = (float_t)FONT_DEFAULT.tileset->tileHeight; - errorChain(textDraw(0, y, text, COLOR_GREEN, &FONT_DEFAULT)); + float_t y = (float_t)SCREEN.scanY + + (float_t)FONT_DEFAULT.tileset->tileHeight; + errorChain(textDraw( + (float_t)SCREEN.scanX, y, text, COLOR_GREEN, &FONT_DEFAULT + )); return spriteBatchFlush(); }