很多修改
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
set(DESKTOP_TARGET IMX6U-Desktop)
|
||||
set(DESKTOP_ATLAS_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/generated/desktop_atlas.h")
|
||||
set_source_files_properties(${DESKTOP_ATLAS_HEADER} PROPERTIES GENERATED TRUE)
|
||||
|
||||
add_executable(${DESKTOP_TARGET}
|
||||
Main.cpp
|
||||
src/app/DesktopApp.cpp
|
||||
src/ui/GameCatalog.cpp
|
||||
src/ui/GameCarousel.cpp
|
||||
src/ui/GameLauncher.cpp
|
||||
${DESKTOP_ATLAS_HEADER}
|
||||
)
|
||||
|
||||
set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/src/ui/GameCatalog.cpp
|
||||
PROPERTIES OBJECT_DEPENDS "${DESKTOP_ATLAS_HEADER}"
|
||||
)
|
||||
|
||||
target_include_directories(${DESKTOP_TARGET} PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/app
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/ui
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/generated
|
||||
)
|
||||
|
||||
imx6u_configure_app_target(${DESKTOP_TARGET})
|
||||
|
||||
if(CMAKE_CROSSCOMPILING AND NOT EXISTS "${DESKTOP_ATLAS_HEADER}")
|
||||
message(FATAL_ERROR
|
||||
"Desktop atlas header is missing. Run GenerateDesktopAtlasHeader in a host build before cross compiling."
|
||||
)
|
||||
endif()
|
||||
|
||||
if(NOT TARGET_IMX)
|
||||
set(DESKTOP_ATLAS_TOOL_SOURCES
|
||||
tools/asset_pipeline/DesktopAssetTool.cpp
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
add_executable(DesktopAtlasTool EXCLUDE_FROM_ALL ${DESKTOP_ATLAS_TOOL_SOURCES})
|
||||
target_include_directories(DesktopAtlasTool PRIVATE
|
||||
${PROJECT_SOURCE_DIR}/libs/Win/SDL2/include
|
||||
${PROJECT_SOURCE_DIR}/libs/Win/SDL_image/include
|
||||
)
|
||||
target_link_directories(DesktopAtlasTool PRIVATE
|
||||
${SDL2_LIB_DIR}
|
||||
${SDL2_IMAGE_LIB_DIR}
|
||||
)
|
||||
target_link_libraries(DesktopAtlasTool PRIVATE SDL2main SDL2 SDL2_image)
|
||||
|
||||
add_custom_command(TARGET DesktopAtlasTool POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${SDL2_DLL}"
|
||||
"$<TARGET_FILE_DIR:DesktopAtlasTool>"
|
||||
)
|
||||
add_custom_command(TARGET DesktopAtlasTool POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${SDL2_IMAGE_DLL}"
|
||||
"$<TARGET_FILE_DIR:DesktopAtlasTool>"
|
||||
)
|
||||
elseif(SDL2_image_FOUND)
|
||||
add_executable(DesktopAtlasTool EXCLUDE_FROM_ALL ${DESKTOP_ATLAS_TOOL_SOURCES})
|
||||
target_include_directories(DesktopAtlasTool PRIVATE
|
||||
${SDL2_image_INCLUDE_DIRS}
|
||||
${SDL2_IMAGE_INCLUDE_DIRS}
|
||||
)
|
||||
target_link_libraries(DesktopAtlasTool PRIVATE SDL2::SDL2)
|
||||
|
||||
if(TARGET SDL2_image::SDL2_image)
|
||||
target_link_libraries(DesktopAtlasTool PRIVATE SDL2_image::SDL2_image)
|
||||
else()
|
||||
target_link_libraries(DesktopAtlasTool PRIVATE
|
||||
${SDL2_image_LIBRARIES}
|
||||
${SDL2_IMAGE_LIBRARIES}
|
||||
)
|
||||
endif()
|
||||
else()
|
||||
message(STATUS "DesktopAtlasTool disabled: SDL2_image was not found")
|
||||
endif()
|
||||
|
||||
if(TARGET DesktopAtlasTool)
|
||||
if(NOT CMAKE_CROSSCOMPILING)
|
||||
add_custom_target(GenerateDesktopAtlasHeader
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory
|
||||
"src/Apps/Desktop/generated"
|
||||
COMMAND $<TARGET_FILE:DesktopAtlasTool>
|
||||
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
|
||||
DEPENDS DesktopAtlasTool
|
||||
COMMENT "Generating desktop atlas header from PNG assets"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
add_dependencies(${DESKTOP_TARGET} GenerateDesktopAtlasHeader)
|
||||
else()
|
||||
add_custom_target(GenerateDesktopAtlasHeader
|
||||
COMMAND ${CMAKE_COMMAND} -E echo
|
||||
"GenerateDesktopAtlasHeader is host-only. Run it in build-win or build-linux before cross compiling."
|
||||
VERBATIM
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
@@ -0,0 +1,150 @@
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
#include "DefaultHardware.h"
|
||||
#include "DrawContext.h"
|
||||
#include "IDisplay.h"
|
||||
#include "TimeSource.h"
|
||||
#include "Timer.h"
|
||||
#include "app/DesktopApp.h"
|
||||
#include "ui/GameLauncher.h"
|
||||
|
||||
#ifdef TARGET_IMX
|
||||
#include "FBDisplay.h"
|
||||
#endif
|
||||
#ifdef TARGET_PC
|
||||
#include "SDLDisplay.h"
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
const int32_t ScreenWidth = 1024;
|
||||
const int32_t ScreenHeight = 600;
|
||||
const int32_t RestoreRetryCount = 5;
|
||||
const uint32_t RestoreRetryDelayMs = 300;
|
||||
|
||||
static Platform::IDisplay* CreateDisplay()
|
||||
{
|
||||
#ifdef TARGET_IMX
|
||||
return new Platform::FBDisplay();
|
||||
#endif
|
||||
#ifdef TARGET_PC
|
||||
return new Platform::SDLDisplay();
|
||||
#endif
|
||||
}
|
||||
|
||||
static void SleepRemainingFrameTime(const Core::Timer& timer, const Platform::ITimeSource& timeSource)
|
||||
{
|
||||
const uint32_t sleepMs = timer.remaining_frame_ms(timeSource.get_time_ms());
|
||||
if (sleepMs > 0u)
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleepMs));
|
||||
}
|
||||
}
|
||||
|
||||
static bool RestoreDesktopHardware(
|
||||
Platform::IDisplay* display,
|
||||
Platform::DefaultPointerInput& pointerInput)
|
||||
{
|
||||
for (int32_t attempt = 1; attempt <= RestoreRetryCount; ++attempt)
|
||||
{
|
||||
if (display->init(ScreenWidth, ScreenHeight))
|
||||
{
|
||||
if (!pointerInput.init("", ScreenWidth, ScreenHeight))
|
||||
{
|
||||
std::cerr << "[WARN] Pointer input restore failed; desktop touch is disabled." << std::endl;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << "[WARN] Failed to restore desktop display, retry "
|
||||
<< attempt << "/" << RestoreRetryCount << "." << std::endl;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(RestoreRetryDelayMs));
|
||||
}
|
||||
|
||||
std::cerr << "[ERROR] Failed to restore desktop display after retries." << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool LaunchGameAndRestoreDesktop(
|
||||
const Desktop::GameEntry& game,
|
||||
Desktop::GameLauncher& launcher,
|
||||
Platform::IDisplay* display,
|
||||
Platform::DefaultPointerInput& pointerInput)
|
||||
{
|
||||
pointerInput.shutdown();
|
||||
display->shutdown();
|
||||
|
||||
const bool launched = launcher.launch_and_wait(game);
|
||||
if (!launched)
|
||||
{
|
||||
std::cerr << "[WARN] Game launch failed: " << (game.title ? game.title : "(unnamed)") << std::endl;
|
||||
}
|
||||
|
||||
return RestoreDesktopHardware(display, pointerInput);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
Platform::IDisplay* display = CreateDisplay();
|
||||
if (!display->init(ScreenWidth, ScreenHeight))
|
||||
{
|
||||
delete display;
|
||||
return -1;
|
||||
}
|
||||
|
||||
Platform::DefaultPointerInput pointerInput;
|
||||
if (!pointerInput.init("", ScreenWidth, ScreenHeight))
|
||||
{
|
||||
std::cerr << "[WARN] Pointer input init failed; desktop touch is disabled." << std::endl;
|
||||
}
|
||||
|
||||
Core::DrawContext ctx(ScreenWidth, ScreenHeight);
|
||||
Core::Timer timer(30);
|
||||
Platform::SteadyTimeSource timeSource;
|
||||
Desktop::DesktopApp app(ScreenWidth, ScreenHeight, &pointerInput);
|
||||
Desktop::GameLauncher launcher;
|
||||
|
||||
std::cout << "[INFO] Desktop started. Target FPS: " << timer.target_fps() << std::endl;
|
||||
|
||||
bool isRunning = true;
|
||||
while (isRunning)
|
||||
{
|
||||
timer.begin_frame(timeSource.get_time_ms());
|
||||
|
||||
bool shouldQuit = false;
|
||||
display->poll_events(shouldQuit);
|
||||
if (shouldQuit)
|
||||
{
|
||||
isRunning = false;
|
||||
}
|
||||
|
||||
app.update(timer.fixed_delta_ms());
|
||||
const Desktop::GameEntry* launchGame = app.consume_pending_launch();
|
||||
if (launchGame != nullptr)
|
||||
{
|
||||
if (!LaunchGameAndRestoreDesktop(*launchGame, launcher, display, pointerInput))
|
||||
{
|
||||
isRunning = false;
|
||||
break;
|
||||
}
|
||||
timer.begin_frame(timeSource.get_time_ms());
|
||||
}
|
||||
|
||||
app.draw(ctx);
|
||||
ctx.present(display);
|
||||
SleepRemainingFrameTime(timer, timeSource);
|
||||
}
|
||||
|
||||
pointerInput.shutdown();
|
||||
display->shutdown();
|
||||
delete display;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
IMX6U Desktop
|
||||
=============
|
||||
|
||||
Game-console desktop application for the 1024x600 screen.
|
||||
|
||||
Interaction:
|
||||
- Tap the left or right screen edge to switch games.
|
||||
- Swipe horizontally to switch games.
|
||||
- Tap the centered cover card to launch the selected game.
|
||||
|
||||
Adding a game:
|
||||
1. Add a PNG cover to `assets/raw`.
|
||||
2. Add it to `DesktopAssetTool.cpp`.
|
||||
3. Add a `GameEntry` in `src/ui/GameCatalog.cpp`.
|
||||
4. Rebuild the Desktop target to regenerate `generated/desktop_atlas.h`.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.5 MiB |
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,58 @@
|
||||
#include "DesktopApp.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "DrawContext.h"
|
||||
#include "PointerInput.h"
|
||||
#include "font_atlas.h"
|
||||
|
||||
namespace Desktop
|
||||
{
|
||||
DesktopApp::DesktopApp(int32_t width, int32_t height, Platform::IPointerInput* input)
|
||||
: pointerInput(input),
|
||||
screenWidth(width),
|
||||
screenHeight(height),
|
||||
pendingLaunchGame(nullptr)
|
||||
{
|
||||
font.mask_bits = font_atlas_mask;
|
||||
font.atlas_width = font_atlas_width;
|
||||
font.atlas_height = font_atlas_height;
|
||||
font.char_w = font_char_w;
|
||||
font.char_h = font_char_h;
|
||||
font.columns = font_columns;
|
||||
font.first_char = font_first_char;
|
||||
|
||||
carousel.configure(GetGameCatalog(), GetGameCatalogCount(), screenWidth, screenHeight);
|
||||
}
|
||||
|
||||
void DesktopApp::update(uint32_t deltaMs)
|
||||
{
|
||||
if (pointerInput != nullptr)
|
||||
{
|
||||
pointerInput->update();
|
||||
}
|
||||
|
||||
const CarouselAction action = carousel.update(deltaMs, pointerInput);
|
||||
if (action == CarouselAction::Launch)
|
||||
{
|
||||
const GameEntry* game = carousel.get_current_game();
|
||||
if (game != nullptr)
|
||||
{
|
||||
std::cout << "[INFO] Launching " << game->title << std::endl;
|
||||
pendingLaunchGame = game;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopApp::draw(Core::DrawContext& ctx)
|
||||
{
|
||||
carousel.draw(ctx, font);
|
||||
}
|
||||
|
||||
const GameEntry* DesktopApp::consume_pending_launch()
|
||||
{
|
||||
const GameEntry* game = pendingLaunchGame;
|
||||
pendingLaunchGame = nullptr;
|
||||
return game;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "BitmapFont.h"
|
||||
#include "GameCatalog.h"
|
||||
#include "GameCarousel.h"
|
||||
|
||||
namespace Core
|
||||
{
|
||||
class DrawContext;
|
||||
}
|
||||
|
||||
namespace Platform
|
||||
{
|
||||
class IPointerInput;
|
||||
}
|
||||
|
||||
namespace Desktop
|
||||
{
|
||||
class DesktopApp
|
||||
{
|
||||
private:
|
||||
Platform::IPointerInput* pointerInput;
|
||||
GameCarousel carousel;
|
||||
RenderData::BitmapFont font;
|
||||
int32_t screenWidth;
|
||||
int32_t screenHeight;
|
||||
const GameEntry* pendingLaunchGame;
|
||||
|
||||
public:
|
||||
DesktopApp(int32_t width, int32_t height, Platform::IPointerInput* pointerInput);
|
||||
|
||||
void update(uint32_t deltaMs);
|
||||
void draw(Core::DrawContext& ctx);
|
||||
const GameEntry* consume_pending_launch();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
#include "GameCarousel.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "Color.h"
|
||||
#include "DrawContext.h"
|
||||
#include "PointerInput.h"
|
||||
|
||||
namespace Desktop
|
||||
{
|
||||
namespace
|
||||
{
|
||||
const int32_t EdgeHotZone = 120;
|
||||
const int32_t SwipeThreshold = 120;
|
||||
const int32_t TapSlop = 18;
|
||||
const uint32_t SlideDurationMs = 220;
|
||||
|
||||
static int32_t AbsInt(int32_t value)
|
||||
{
|
||||
return value < 0 ? -value : value;
|
||||
}
|
||||
|
||||
static int32_t LerpInt(int32_t from, int32_t to, uint32_t elapsed, uint32_t duration)
|
||||
{
|
||||
if (duration == 0 || elapsed >= duration)
|
||||
{
|
||||
return to;
|
||||
}
|
||||
const int32_t delta = to - from;
|
||||
return from + static_cast<int32_t>((static_cast<int64_t>(delta) * static_cast<int64_t>(elapsed)) / static_cast<int64_t>(duration));
|
||||
}
|
||||
}
|
||||
|
||||
GameCarousel::GameCarousel()
|
||||
: games(nullptr),
|
||||
gameCount(0),
|
||||
currentIndex(0),
|
||||
targetIndex(0),
|
||||
screenWidth(0),
|
||||
screenHeight(0),
|
||||
dragStartX(0),
|
||||
dragCurrentX(0),
|
||||
dragStartY(0),
|
||||
dragging(false),
|
||||
pointerWasDown(false),
|
||||
animationFromOffset(0),
|
||||
animationToOffset(0),
|
||||
animationElapsedMs(0),
|
||||
animationDurationMs(SlideDurationMs)
|
||||
{
|
||||
}
|
||||
|
||||
void GameCarousel::configure(const GameEntry* catalog, size_t catalogCount, int32_t width, int32_t height)
|
||||
{
|
||||
games = catalog;
|
||||
gameCount = catalogCount;
|
||||
screenWidth = width;
|
||||
screenHeight = height;
|
||||
currentIndex = 0;
|
||||
targetIndex = 0;
|
||||
}
|
||||
|
||||
size_t GameCarousel::previous_index(size_t index) const
|
||||
{
|
||||
if (gameCount == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return index == 0 ? gameCount - 1 : index - 1;
|
||||
}
|
||||
|
||||
size_t GameCarousel::next_index(size_t index) const
|
||||
{
|
||||
return gameCount == 0 ? 0 : (index + 1) % gameCount;
|
||||
}
|
||||
|
||||
int32_t GameCarousel::get_card_width() const
|
||||
{
|
||||
return screenWidth - 144;
|
||||
}
|
||||
|
||||
int32_t GameCarousel::get_card_height() const
|
||||
{
|
||||
return screenHeight - 156;
|
||||
}
|
||||
|
||||
int32_t GameCarousel::get_card_x() const
|
||||
{
|
||||
return (screenWidth - get_card_width()) / 2;
|
||||
}
|
||||
|
||||
int32_t GameCarousel::get_card_y() const
|
||||
{
|
||||
return 70;
|
||||
}
|
||||
|
||||
bool GameCarousel::is_animating() const
|
||||
{
|
||||
return animationElapsedMs < animationDurationMs && currentIndex != targetIndex;
|
||||
}
|
||||
|
||||
int32_t GameCarousel::get_current_offset() const
|
||||
{
|
||||
if (dragging)
|
||||
{
|
||||
return dragCurrentX - dragStartX;
|
||||
}
|
||||
if (is_animating())
|
||||
{
|
||||
return LerpInt(animationFromOffset, animationToOffset, animationElapsedMs, animationDurationMs);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool GameCarousel::is_inside_card(int32_t x, int32_t y) const
|
||||
{
|
||||
const int32_t cardX = get_card_x();
|
||||
const int32_t cardY = get_card_y();
|
||||
return x >= cardX && x < cardX + get_card_width() && y >= cardY && y < cardY + get_card_height();
|
||||
}
|
||||
|
||||
void GameCarousel::begin_slide(size_t index, int32_t direction)
|
||||
{
|
||||
if (gameCount <= 1 || index == currentIndex)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
targetIndex = index;
|
||||
animationFromOffset = direction * screenWidth;
|
||||
animationToOffset = 0;
|
||||
animationElapsedMs = 0;
|
||||
animationDurationMs = SlideDurationMs;
|
||||
}
|
||||
|
||||
CarouselAction GameCarousel::update(uint32_t deltaMs, Platform::IPointerInput* pointerInput)
|
||||
{
|
||||
if (is_animating())
|
||||
{
|
||||
animationElapsedMs += deltaMs;
|
||||
if (animationElapsedMs >= animationDurationMs)
|
||||
{
|
||||
currentIndex = targetIndex;
|
||||
animationElapsedMs = animationDurationMs;
|
||||
}
|
||||
}
|
||||
|
||||
if (gameCount == 0 || pointerInput == nullptr)
|
||||
{
|
||||
return CarouselAction::None;
|
||||
}
|
||||
|
||||
const bool down = pointerInput->is_down();
|
||||
const int32_t x = pointerInput->get_x();
|
||||
const int32_t y = pointerInput->get_y();
|
||||
|
||||
if (down && !pointerWasDown && !is_animating())
|
||||
{
|
||||
dragging = true;
|
||||
dragStartX = x;
|
||||
dragCurrentX = x;
|
||||
dragStartY = y;
|
||||
}
|
||||
else if (down && dragging)
|
||||
{
|
||||
dragCurrentX = x;
|
||||
}
|
||||
else if (!down && pointerWasDown && dragging)
|
||||
{
|
||||
const int32_t dx = x - dragStartX;
|
||||
const int32_t dy = y - dragStartY;
|
||||
dragging = false;
|
||||
|
||||
if (is_animating())
|
||||
{
|
||||
pointerWasDown = down;
|
||||
return CarouselAction::None;
|
||||
}
|
||||
|
||||
if (AbsInt(dx) > SwipeThreshold && AbsInt(dx) > AbsInt(dy))
|
||||
{
|
||||
begin_slide(dx < 0 ? next_index(currentIndex) : previous_index(currentIndex), dx < 0 ? 1 : -1);
|
||||
}
|
||||
else if (AbsInt(dx) <= TapSlop && AbsInt(dy) <= TapSlop)
|
||||
{
|
||||
if (dragStartX < EdgeHotZone)
|
||||
{
|
||||
begin_slide(previous_index(currentIndex), -1);
|
||||
}
|
||||
else if (dragStartX > screenWidth - EdgeHotZone)
|
||||
{
|
||||
begin_slide(next_index(currentIndex), 1);
|
||||
}
|
||||
else if (is_inside_card(dragStartX, dragStartY))
|
||||
{
|
||||
pointerWasDown = down;
|
||||
return CarouselAction::Launch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pointerWasDown = down;
|
||||
return CarouselAction::None;
|
||||
}
|
||||
|
||||
void GameCarousel::draw_card(Core::DrawContext& ctx, const GameEntry& game, int32_t x, int32_t y, bool active) const
|
||||
{
|
||||
const int32_t cardW = get_card_width();
|
||||
const int32_t cardH = get_card_height();
|
||||
const RenderData::Color shadow(0, 0, 0, 255);
|
||||
const RenderData::Color border = active ? RenderData::Color(250, 230, 145, 255) : RenderData::Color(74, 82, 105, 255);
|
||||
const RenderData::Color panel(20, 24, 36, 255);
|
||||
|
||||
ctx.fill_rect(x + 12, y + 14, cardW, cardH, shadow);
|
||||
ctx.fill_rect(x - 4, y - 4, cardW + 8, cardH + 8, border);
|
||||
ctx.fill_rect(x, y, cardW, cardH, panel);
|
||||
|
||||
if (game.cover != nullptr && game.cover->atlas != nullptr)
|
||||
{
|
||||
const int32_t coverX = x + (cardW - game.cover->width) / 2;
|
||||
const int32_t coverY = y + (cardH - game.cover->height) / 2;
|
||||
ctx.draw_sprite(coverX, coverY, *game.cover);
|
||||
}
|
||||
else
|
||||
{
|
||||
ctx.fill_rect(x + 28, y + 28, cardW - 56, cardH - 56, RenderData::Color(48, 56, 82, 255));
|
||||
}
|
||||
}
|
||||
|
||||
void GameCarousel::draw_text_centered(Core::DrawContext& ctx, const RenderData::BitmapFont& font, int32_t y, const char* text) const
|
||||
{
|
||||
if (text == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t len = 0;
|
||||
for (const char* p = text; *p; ++p)
|
||||
{
|
||||
++len;
|
||||
}
|
||||
|
||||
const int32_t x = (screenWidth - len * font.char_w) / 2;
|
||||
ctx.draw_text(font, x, y, RenderData::Color(245, 248, 255, 255), text);
|
||||
}
|
||||
|
||||
void GameCarousel::draw_edge_hints(Core::DrawContext& ctx) const
|
||||
{
|
||||
ctx.fill_rect(0, 0, 42, screenHeight, RenderData::Color(9, 12, 20, 255));
|
||||
ctx.fill_rect(screenWidth - 42, 0, 42, screenHeight, RenderData::Color(9, 12, 20, 255));
|
||||
|
||||
const int32_t cy = screenHeight / 2;
|
||||
for (int32_t i = 0; i < 24; ++i)
|
||||
{
|
||||
ctx.fill_rect(24 - i / 2, cy - 24 + i, 3, 3, RenderData::Color(154, 168, 210, 255));
|
||||
ctx.fill_rect(screenWidth - 27 + i / 2, cy - 24 + i, 3, 3, RenderData::Color(154, 168, 210, 255));
|
||||
}
|
||||
}
|
||||
|
||||
void GameCarousel::draw_page_dots(Core::DrawContext& ctx) const
|
||||
{
|
||||
if (gameCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const int32_t dotSize = 10;
|
||||
const int32_t gap = 18;
|
||||
const int32_t totalW = static_cast<int32_t>(gameCount) * dotSize + static_cast<int32_t>(gameCount - 1) * gap;
|
||||
const int32_t startX = (screenWidth - totalW) / 2;
|
||||
const int32_t y = screenHeight - 42;
|
||||
|
||||
for (size_t i = 0; i < gameCount; ++i)
|
||||
{
|
||||
const bool active = i == currentIndex || (is_animating() && i == targetIndex);
|
||||
const RenderData::Color color = active ? RenderData::Color(250, 230, 145, 255) : RenderData::Color(90, 100, 128, 255);
|
||||
ctx.fill_rect(startX + static_cast<int32_t>(i) * (dotSize + gap), y, dotSize, dotSize, color);
|
||||
}
|
||||
}
|
||||
|
||||
void GameCarousel::draw(Core::DrawContext& ctx, const RenderData::BitmapFont& font) const
|
||||
{
|
||||
ctx.clear_color(RenderData::Color(10, 14, 24, 255));
|
||||
ctx.fill_rect(0, 0, screenWidth, 56, RenderData::Color(16, 22, 35, 255));
|
||||
draw_text_centered(ctx, font, 20, "IMX6U GAME DESKTOP");
|
||||
|
||||
if (gameCount == 0)
|
||||
{
|
||||
draw_text_centered(ctx, font, screenHeight / 2, "NO GAMES");
|
||||
return;
|
||||
}
|
||||
|
||||
draw_edge_hints(ctx);
|
||||
|
||||
const int32_t baseX = get_card_x();
|
||||
const int32_t baseY = get_card_y();
|
||||
const int32_t offset = get_current_offset();
|
||||
|
||||
if (is_animating())
|
||||
{
|
||||
const int32_t currentOffset = animationFromOffset > 0 ? offset - screenWidth : offset + screenWidth;
|
||||
draw_card(ctx, games[currentIndex], baseX + currentOffset, baseY, false);
|
||||
draw_card(ctx, games[targetIndex], baseX + offset, baseY, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (dragging && gameCount > 1)
|
||||
{
|
||||
if (offset > 0)
|
||||
{
|
||||
draw_card(ctx, games[previous_index(currentIndex)], baseX + offset - screenWidth, baseY, false);
|
||||
}
|
||||
else if (offset < 0)
|
||||
{
|
||||
draw_card(ctx, games[next_index(currentIndex)], baseX + offset + screenWidth, baseY, false);
|
||||
}
|
||||
}
|
||||
draw_card(ctx, games[currentIndex], baseX + offset, baseY, true);
|
||||
}
|
||||
|
||||
draw_text_centered(ctx, font, screenHeight - 78, games[currentIndex].title);
|
||||
draw_page_dots(ctx);
|
||||
}
|
||||
|
||||
const GameEntry* GameCarousel::get_current_game() const
|
||||
{
|
||||
if (games == nullptr || gameCount == 0)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return &games[currentIndex];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "BitmapFont.h"
|
||||
#include "GameCatalog.h"
|
||||
|
||||
namespace Core
|
||||
{
|
||||
class DrawContext;
|
||||
}
|
||||
|
||||
namespace Platform
|
||||
{
|
||||
class IPointerInput;
|
||||
}
|
||||
|
||||
namespace Desktop
|
||||
{
|
||||
enum class CarouselAction
|
||||
{
|
||||
None,
|
||||
Launch
|
||||
};
|
||||
|
||||
class GameCarousel
|
||||
{
|
||||
private:
|
||||
const GameEntry* games;
|
||||
size_t gameCount;
|
||||
size_t currentIndex;
|
||||
size_t targetIndex;
|
||||
int32_t screenWidth;
|
||||
int32_t screenHeight;
|
||||
int32_t dragStartX;
|
||||
int32_t dragCurrentX;
|
||||
int32_t dragStartY;
|
||||
bool dragging;
|
||||
bool pointerWasDown;
|
||||
int32_t animationFromOffset;
|
||||
int32_t animationToOffset;
|
||||
uint32_t animationElapsedMs;
|
||||
uint32_t animationDurationMs;
|
||||
|
||||
size_t previous_index(size_t index) const;
|
||||
size_t next_index(size_t index) const;
|
||||
int32_t get_card_width() const;
|
||||
int32_t get_card_height() const;
|
||||
int32_t get_card_x() const;
|
||||
int32_t get_card_y() const;
|
||||
int32_t get_current_offset() const;
|
||||
bool is_animating() const;
|
||||
bool is_inside_card(int32_t x, int32_t y) const;
|
||||
void begin_slide(size_t index, int32_t direction);
|
||||
void draw_card(Core::DrawContext& ctx, const GameEntry& game, int32_t x, int32_t y, bool active) const;
|
||||
void draw_text_centered(Core::DrawContext& ctx, const RenderData::BitmapFont& font, int32_t y, const char* text) const;
|
||||
void draw_edge_hints(Core::DrawContext& ctx) const;
|
||||
void draw_page_dots(Core::DrawContext& ctx) const;
|
||||
|
||||
public:
|
||||
GameCarousel();
|
||||
|
||||
void configure(const GameEntry* catalog, size_t catalogCount, int32_t width, int32_t height);
|
||||
CarouselAction update(uint32_t deltaMs, Platform::IPointerInput* pointerInput);
|
||||
void draw(Core::DrawContext& ctx, const RenderData::BitmapFont& font) const;
|
||||
const GameEntry* get_current_game() const;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#include "GameCatalog.h"
|
||||
|
||||
#include "desktop_atlas.h"
|
||||
|
||||
namespace Desktop
|
||||
{
|
||||
namespace
|
||||
{
|
||||
const GameEntry Games[] = {
|
||||
{ "tom", "TomGame", &DesktopAtlas::TomGame, "IMX6U-Game", true },
|
||||
{ "light", "LightGame", &DesktopAtlas::LightGame, "IMX6U-LightGame", true },
|
||||
{ "demo", "Demo", &DesktopAtlas::Demo, "IMX6U-Demo", true },
|
||||
};
|
||||
}
|
||||
|
||||
const GameEntry* GetGameCatalog()
|
||||
{
|
||||
return Games;
|
||||
}
|
||||
|
||||
size_t GetGameCatalogCount()
|
||||
{
|
||||
return sizeof(Games) / sizeof(Games[0]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
#include "Sprite.h"
|
||||
|
||||
namespace Desktop
|
||||
{
|
||||
struct GameEntry
|
||||
{
|
||||
const char* id;
|
||||
const char* title;
|
||||
const RenderData::Sprite* cover;
|
||||
const char* executable;
|
||||
bool enabled;
|
||||
};
|
||||
|
||||
const GameEntry* GetGameCatalog();
|
||||
size_t GetGameCatalogCount();
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
#include "GameLauncher.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <direct.h>
|
||||
#include <windows.h>
|
||||
#include <process.h>
|
||||
#else
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace Desktop
|
||||
{
|
||||
namespace
|
||||
{
|
||||
static bool HasExeExtension(const std::string& command)
|
||||
{
|
||||
if (command.size() < 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const size_t offset = command.size() - 4;
|
||||
return (command[offset] == '.') &&
|
||||
(command[offset + 1] == 'e' || command[offset + 1] == 'E') &&
|
||||
(command[offset + 2] == 'x' || command[offset + 2] == 'X') &&
|
||||
(command[offset + 3] == 'e' || command[offset + 3] == 'E');
|
||||
}
|
||||
|
||||
static std::string BuildExecutableName(const char* executable)
|
||||
{
|
||||
std::string command = executable ? executable : "";
|
||||
#ifdef _WIN32
|
||||
if (!HasExeExtension(command))
|
||||
{
|
||||
command += ".exe";
|
||||
}
|
||||
#endif
|
||||
return command;
|
||||
}
|
||||
|
||||
static std::string GetExecutableDirectory()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
char path[MAX_PATH];
|
||||
const DWORD length = GetModuleFileNameA(nullptr, path, MAX_PATH);
|
||||
if (length == 0 || length >= MAX_PATH)
|
||||
{
|
||||
char cwd[1024];
|
||||
return _getcwd(cwd, sizeof(cwd)) == nullptr ? std::string(".") : std::string(cwd);
|
||||
}
|
||||
std::string fullPath(path, length);
|
||||
const size_t slash = fullPath.find_last_of("\\/");
|
||||
return slash == std::string::npos ? std::string(".") : fullPath.substr(0, slash);
|
||||
#else
|
||||
char path[PATH_MAX];
|
||||
const ssize_t length = readlink("/proc/self/exe", path, sizeof(path) - 1);
|
||||
if (length <= 0)
|
||||
{
|
||||
char cwd[PATH_MAX];
|
||||
return getcwd(cwd, sizeof(cwd)) == nullptr ? std::string(".") : std::string(cwd);
|
||||
}
|
||||
path[length] = '\0';
|
||||
std::string fullPath(path);
|
||||
const size_t slash = fullPath.find_last_of('/');
|
||||
return slash == std::string::npos ? std::string(".") : fullPath.substr(0, slash);
|
||||
#endif
|
||||
}
|
||||
|
||||
static std::string BuildLocalExecutablePath(const std::string& executable)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
return GetExecutableDirectory() + "\\" + executable;
|
||||
#else
|
||||
return GetExecutableDirectory() + "/" + executable;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
bool GameLauncher::launch_and_wait(const GameEntry& game) const
|
||||
{
|
||||
if (!game.enabled || game.executable == nullptr || game.executable[0] == '\0')
|
||||
{
|
||||
std::cerr << "[WARN] Game is not launchable: " << (game.title ? game.title : "(unnamed)") << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string executable = BuildExecutableName(game.executable);
|
||||
const std::string localPath = BuildLocalExecutablePath(executable);
|
||||
|
||||
#ifdef _WIN32
|
||||
intptr_t result = _spawnl(_P_WAIT, localPath.c_str(), executable.c_str(), nullptr);
|
||||
if (result == -1)
|
||||
{
|
||||
result = _spawnlp(_P_WAIT, executable.c_str(), executable.c_str(), nullptr);
|
||||
}
|
||||
if (result == -1)
|
||||
{
|
||||
std::cerr << "[WARN] Failed to launch " << executable << std::endl;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
const pid_t pid = fork();
|
||||
if (pid < 0)
|
||||
{
|
||||
std::cerr << "[WARN] fork failed for " << executable << std::endl;
|
||||
return false;
|
||||
}
|
||||
if (pid == 0)
|
||||
{
|
||||
execl(localPath.c_str(), executable.c_str(), static_cast<char*>(nullptr));
|
||||
execlp(executable.c_str(), executable.c_str(), static_cast<char*>(nullptr));
|
||||
std::exit(127);
|
||||
}
|
||||
|
||||
int status = 0;
|
||||
while (waitpid(pid, &status, 0) < 0)
|
||||
{
|
||||
if (errno != EINTR)
|
||||
{
|
||||
std::cerr << "[WARN] waitpid failed for " << executable << std::endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "GameCatalog.h"
|
||||
|
||||
namespace Desktop
|
||||
{
|
||||
class GameLauncher
|
||||
{
|
||||
public:
|
||||
bool launch_and_wait(const GameEntry& game) const;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
#include <SDL.h>
|
||||
#include <SDL_image.h>
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
const char* InputDirectory = "src/Apps/Desktop/assets/raw";
|
||||
const char* OutputHeaderPath = "src/Apps/Desktop/generated/desktop_atlas.h";
|
||||
const int32_t AtlasWidth = 1024;
|
||||
const int32_t AtlasPadding = 1;
|
||||
const int32_t CoverMaxWidth = 860;
|
||||
const int32_t CoverMaxHeight = 420;
|
||||
|
||||
struct SourceSpec
|
||||
{
|
||||
const char* file_name;
|
||||
const char* region_name;
|
||||
bool allow_generated_fallback;
|
||||
};
|
||||
|
||||
const SourceSpec Sources[] = {
|
||||
{ "TomGame.png", "TomGame", false },
|
||||
{ "LightGame.png", "LightGame", false },
|
||||
{ "Demo.png", "Demo", true },
|
||||
};
|
||||
|
||||
struct Image
|
||||
{
|
||||
std::vector<uint16_t> pixels;
|
||||
int32_t width;
|
||||
int32_t height;
|
||||
|
||||
Image() : width(0), height(0) {}
|
||||
|
||||
bool is_valid() const
|
||||
{
|
||||
return width > 0 && height > 0 && !pixels.empty();
|
||||
}
|
||||
};
|
||||
|
||||
struct AtlasRegion
|
||||
{
|
||||
const SourceSpec* source;
|
||||
Image image;
|
||||
int32_t x;
|
||||
int32_t y;
|
||||
|
||||
AtlasRegion() : source(nullptr), x(0), y(0) {}
|
||||
};
|
||||
|
||||
static std::string JoinPath(const std::string& directory, const std::string& fileName)
|
||||
{
|
||||
if (directory.empty())
|
||||
{
|
||||
return fileName;
|
||||
}
|
||||
const char last = directory[directory.size() - 1];
|
||||
if (last == '/' || last == '\\')
|
||||
{
|
||||
return directory + fileName;
|
||||
}
|
||||
return directory + "/" + fileName;
|
||||
}
|
||||
|
||||
static uint16_t PackRgba5551(uint8_t r, uint8_t g, uint8_t b, uint8_t a)
|
||||
{
|
||||
return static_cast<uint16_t>(
|
||||
((static_cast<uint16_t>(r) >> 3) << 11) |
|
||||
((static_cast<uint16_t>(g) >> 3) << 6) |
|
||||
((static_cast<uint16_t>(b) >> 3) << 1) |
|
||||
(a ? 1u : 0u));
|
||||
}
|
||||
|
||||
static Uint32 ReadSurfacePixel(const SDL_Surface* surface, int32_t x, int32_t y)
|
||||
{
|
||||
const uint8_t* row = static_cast<const uint8_t*>(surface->pixels) + static_cast<size_t>(y) * static_cast<size_t>(surface->pitch);
|
||||
const uint8_t* src = row + static_cast<size_t>(x) * static_cast<size_t>(surface->format->BytesPerPixel);
|
||||
|
||||
switch (surface->format->BytesPerPixel)
|
||||
{
|
||||
case 1:
|
||||
return src[0];
|
||||
case 2:
|
||||
{
|
||||
uint16_t pixel = 0;
|
||||
std::memcpy(&pixel, src, sizeof(pixel));
|
||||
return static_cast<Uint32>(pixel);
|
||||
}
|
||||
case 3:
|
||||
if (SDL_BYTEORDER == SDL_BIG_ENDIAN)
|
||||
{
|
||||
return (static_cast<Uint32>(src[0]) << 16) | (static_cast<Uint32>(src[1]) << 8) | static_cast<Uint32>(src[2]);
|
||||
}
|
||||
return static_cast<Uint32>(src[0]) | (static_cast<Uint32>(src[1]) << 8) | (static_cast<Uint32>(src[2]) << 16);
|
||||
case 4:
|
||||
{
|
||||
Uint32 pixel = 0;
|
||||
std::memcpy(&pixel, src, sizeof(pixel));
|
||||
return pixel;
|
||||
}
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static bool LoadPngImage(const std::string& path, Image& image)
|
||||
{
|
||||
SDL_Surface* surface = IMG_Load(path.c_str());
|
||||
if (surface == nullptr)
|
||||
{
|
||||
std::cerr << "IMG_Load failed: " << path << " : " << IMG_GetError() << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (SDL_LockSurface(surface) != 0)
|
||||
{
|
||||
std::cerr << "SDL_LockSurface failed: " << SDL_GetError() << std::endl;
|
||||
SDL_FreeSurface(surface);
|
||||
return false;
|
||||
}
|
||||
|
||||
image.width = surface->w;
|
||||
image.height = surface->h;
|
||||
image.pixels.assign(static_cast<size_t>(image.width) * static_cast<size_t>(image.height), 0);
|
||||
|
||||
for (int32_t y = 0; y < image.height; ++y)
|
||||
{
|
||||
for (int32_t x = 0; x < image.width; ++x)
|
||||
{
|
||||
uint8_t r = 0;
|
||||
uint8_t g = 0;
|
||||
uint8_t b = 0;
|
||||
uint8_t a = 0;
|
||||
SDL_GetRGBA(ReadSurfacePixel(surface, x, y), surface->format, &r, &g, &b, &a);
|
||||
image.pixels[static_cast<size_t>(y) * static_cast<size_t>(image.width) + static_cast<size_t>(x)] =
|
||||
a == 0 ? 0u : PackRgba5551(r, g, b, a);
|
||||
}
|
||||
}
|
||||
|
||||
SDL_UnlockSurface(surface);
|
||||
SDL_FreeSurface(surface);
|
||||
return image.is_valid();
|
||||
}
|
||||
|
||||
static Image ResizeNearest(const Image& source, int32_t width, int32_t height)
|
||||
{
|
||||
Image result;
|
||||
if (!source.is_valid() || width <= 0 || height <= 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
result.width = width;
|
||||
result.height = height;
|
||||
result.pixels.assign(static_cast<size_t>(width) * static_cast<size_t>(height), 0);
|
||||
|
||||
for (int32_t y = 0; y < height; ++y)
|
||||
{
|
||||
const int32_t sourceY = y * source.height / height;
|
||||
for (int32_t x = 0; x < width; ++x)
|
||||
{
|
||||
const int32_t sourceX = x * source.width / width;
|
||||
result.pixels[static_cast<size_t>(y) * static_cast<size_t>(width) + static_cast<size_t>(x)] =
|
||||
source.pixels[static_cast<size_t>(sourceY) * static_cast<size_t>(source.width) + static_cast<size_t>(sourceX)];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static Image ResizeToFit(const Image& source, int32_t maxWidth, int32_t maxHeight)
|
||||
{
|
||||
if (!source.is_valid() || maxWidth <= 0 || maxHeight <= 0)
|
||||
{
|
||||
return Image();
|
||||
}
|
||||
|
||||
const float scaleX = static_cast<float>(maxWidth) / static_cast<float>(source.width);
|
||||
const float scaleY = static_cast<float>(maxHeight) / static_cast<float>(source.height);
|
||||
const float scale = std::min(scaleX, scaleY);
|
||||
const int32_t width = std::max(1, static_cast<int32_t>(static_cast<float>(source.width) * scale));
|
||||
const int32_t height = std::max(1, static_cast<int32_t>(static_cast<float>(source.height) * scale));
|
||||
return ResizeNearest(source, width, height);
|
||||
}
|
||||
|
||||
static Image GenerateDemoCover()
|
||||
{
|
||||
Image image;
|
||||
image.width = CoverMaxWidth;
|
||||
image.height = CoverMaxHeight;
|
||||
image.pixels.assign(static_cast<size_t>(image.width) * static_cast<size_t>(image.height), 0u);
|
||||
|
||||
for (int32_t y = 0; y < image.height; ++y)
|
||||
{
|
||||
for (int32_t x = 0; x < image.width; ++x)
|
||||
{
|
||||
const uint8_t r = static_cast<uint8_t>(24 + (x * 48) / image.width);
|
||||
const uint8_t g = static_cast<uint8_t>(36 + (y * 72) / image.height);
|
||||
const uint8_t b = static_cast<uint8_t>(80 + ((x + y) * 64) / (image.width + image.height));
|
||||
image.pixels[static_cast<size_t>(y) * static_cast<size_t>(image.width) + static_cast<size_t>(x)] =
|
||||
PackRgba5551(r, g, b, 255);
|
||||
}
|
||||
}
|
||||
|
||||
const int32_t boxX = image.width / 2 - 170;
|
||||
const int32_t boxY = image.height / 2 - 62;
|
||||
for (int32_t y = 0; y < 124; ++y)
|
||||
{
|
||||
for (int32_t x = 0; x < 340; ++x)
|
||||
{
|
||||
const bool border = x < 6 || y < 6 || x >= 334 || y >= 118;
|
||||
const uint16_t pixel = border ? PackRgba5551(250, 230, 145, 255) : PackRgba5551(18, 24, 42, 255);
|
||||
const int32_t dstX = boxX + x;
|
||||
const int32_t dstY = boxY + y;
|
||||
if (dstX >= 0 && dstX < image.width && dstY >= 0 && dstY < image.height)
|
||||
{
|
||||
image.pixels[static_cast<size_t>(dstY) * static_cast<size_t>(image.width) + static_cast<size_t>(dstX)] = pixel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
static bool LoadAndResizeSource(const SourceSpec& source, AtlasRegion& region)
|
||||
{
|
||||
region.source = &source;
|
||||
Image image;
|
||||
const std::string inputPath = JoinPath(InputDirectory, source.file_name);
|
||||
if (!LoadPngImage(inputPath, image))
|
||||
{
|
||||
if (!source.allow_generated_fallback)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
region.image = GenerateDemoCover();
|
||||
std::cout << "(generated fallback) -> " << source.region_name << " ("
|
||||
<< region.image.width << "x" << region.image.height << ")" << std::endl;
|
||||
return region.image.is_valid();
|
||||
}
|
||||
|
||||
region.image = ResizeToFit(image, CoverMaxWidth, CoverMaxHeight);
|
||||
if (!region.image.is_valid())
|
||||
{
|
||||
std::cerr << "Resize failed: " << inputPath << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << source.file_name << " -> " << source.region_name << " ("
|
||||
<< region.image.width << "x" << region.image.height << ")" << std::endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool PackAtlas(std::vector<AtlasRegion>& regions, int32_t& atlasHeight)
|
||||
{
|
||||
int32_t cursorX = 0;
|
||||
int32_t cursorY = 0;
|
||||
int32_t rowHeight = 0;
|
||||
|
||||
for (size_t i = 0; i < regions.size(); ++i)
|
||||
{
|
||||
AtlasRegion& region = regions[i];
|
||||
if (region.image.width > AtlasWidth)
|
||||
{
|
||||
std::cerr << "AtlasWidth is smaller than source image: " << region.source->file_name << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cursorX > 0 && cursorX + region.image.width > AtlasWidth)
|
||||
{
|
||||
cursorY += rowHeight + AtlasPadding;
|
||||
cursorX = 0;
|
||||
rowHeight = 0;
|
||||
}
|
||||
|
||||
region.x = cursorX;
|
||||
region.y = cursorY;
|
||||
cursorX += region.image.width + AtlasPadding;
|
||||
rowHeight = std::max(rowHeight, region.image.height);
|
||||
}
|
||||
|
||||
atlasHeight = cursorY + rowHeight;
|
||||
return atlasHeight > 0;
|
||||
}
|
||||
|
||||
static std::vector<uint16_t> BuildAtlasPixels(const std::vector<AtlasRegion>& regions, int32_t atlasHeight)
|
||||
{
|
||||
std::vector<uint16_t> atlasPixels(static_cast<size_t>(AtlasWidth) * static_cast<size_t>(atlasHeight), 0u);
|
||||
|
||||
for (size_t i = 0; i < regions.size(); ++i)
|
||||
{
|
||||
const AtlasRegion& region = regions[i];
|
||||
for (int32_t y = 0; y < region.image.height; ++y)
|
||||
{
|
||||
const size_t srcOffset = static_cast<size_t>(y) * static_cast<size_t>(region.image.width);
|
||||
const size_t dstOffset = static_cast<size_t>(region.y + y) * static_cast<size_t>(AtlasWidth) + static_cast<size_t>(region.x);
|
||||
std::copy(
|
||||
region.image.pixels.begin() + static_cast<std::vector<uint16_t>::difference_type>(srcOffset),
|
||||
region.image.pixels.begin() + static_cast<std::vector<uint16_t>::difference_type>(srcOffset + static_cast<size_t>(region.image.width)),
|
||||
atlasPixels.begin() + static_cast<std::vector<uint16_t>::difference_type>(dstOffset));
|
||||
}
|
||||
}
|
||||
|
||||
return atlasPixels;
|
||||
}
|
||||
|
||||
static void WritePixel5551(std::ofstream& file, uint16_t pixel)
|
||||
{
|
||||
file << "0x"
|
||||
<< std::uppercase
|
||||
<< std::hex
|
||||
<< std::setw(4)
|
||||
<< std::setfill('0')
|
||||
<< pixel
|
||||
<< std::dec
|
||||
<< std::nouppercase
|
||||
<< std::setfill(' ');
|
||||
}
|
||||
|
||||
static bool WriteAtlasHeader(const std::vector<AtlasRegion>& regions, const std::vector<uint16_t>& atlasPixels, int32_t atlasHeight)
|
||||
{
|
||||
std::ofstream file(OutputHeaderPath, std::ios::binary);
|
||||
if (!file.good())
|
||||
{
|
||||
std::cerr << "Open output header failed: " << OutputHeaderPath << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
file << "// Auto-generated by DesktopAssetTool.cpp\n";
|
||||
file << "#pragma once\n";
|
||||
file << "#include <cstdint>\n";
|
||||
file << "#include \"Image.h\"\n";
|
||||
file << "#include \"Sprite.h\"\n\n";
|
||||
file << "namespace DesktopAtlas\n";
|
||||
file << "{\n";
|
||||
file << "\tstatic const int32_t desktop_atlas_width = " << AtlasWidth << ";\n";
|
||||
file << "\tstatic const int32_t desktop_atlas_height = " << atlasHeight << ";\n\n";
|
||||
file << "\tstatic const uint16_t desktop_atlas_pixels[] = {\n";
|
||||
|
||||
for (size_t i = 0; i < atlasPixels.size(); i += 12)
|
||||
{
|
||||
file << "\t\t";
|
||||
const size_t end = std::min(i + 12, atlasPixels.size());
|
||||
for (size_t j = i; j < end; ++j)
|
||||
{
|
||||
if (j > i)
|
||||
{
|
||||
file << " ";
|
||||
}
|
||||
WritePixel5551(file, atlasPixels[j]);
|
||||
if (j + 1 < atlasPixels.size())
|
||||
{
|
||||
file << ",";
|
||||
}
|
||||
}
|
||||
file << "\n";
|
||||
}
|
||||
|
||||
file << "\t};\n\n";
|
||||
file << "\tstatic const RenderData::Image image(desktop_atlas_pixels, desktop_atlas_width, desktop_atlas_height, 0x0000, RenderData::PixelFormat::RGBA5551);\n\n";
|
||||
|
||||
for (size_t i = 0; i < regions.size(); ++i)
|
||||
{
|
||||
const AtlasRegion& region = regions[i];
|
||||
file << "\tstatic const RenderData::Sprite "
|
||||
<< region.source->region_name
|
||||
<< "(&image, "
|
||||
<< region.x << ", "
|
||||
<< region.y << ", "
|
||||
<< region.image.width << ", "
|
||||
<< region.image.height << ");\n";
|
||||
}
|
||||
|
||||
file << "}\n";
|
||||
return file.good();
|
||||
}
|
||||
|
||||
static bool GenerateDesktopAtlasHeader()
|
||||
{
|
||||
std::vector<AtlasRegion> regions;
|
||||
const size_t sourceCount = sizeof(Sources) / sizeof(Sources[0]);
|
||||
regions.reserve(sourceCount);
|
||||
|
||||
for (size_t i = 0; i < sourceCount; ++i)
|
||||
{
|
||||
AtlasRegion region;
|
||||
if (!LoadAndResizeSource(Sources[i], region))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
regions.push_back(region);
|
||||
}
|
||||
|
||||
int32_t atlasHeight = 0;
|
||||
if (!PackAtlas(regions, atlasHeight))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::vector<uint16_t> atlasPixels = BuildAtlasPixels(regions, atlasHeight);
|
||||
if (!WriteAtlasHeader(regions, atlasPixels, atlasHeight))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << "Generated " << OutputHeaderPath << " (" << AtlasWidth << "x" << atlasHeight << ", "
|
||||
<< regions.size() << " regions)" << std::endl;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
if (argc > 1)
|
||||
{
|
||||
std::cout << "Usage: DesktopAssetTool\n";
|
||||
std::cout << "Generates " << OutputHeaderPath << " from PNG files in " << InputDirectory << "." << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
const int imageFlags = IMG_INIT_PNG;
|
||||
if ((IMG_Init(imageFlags) & imageFlags) != imageFlags)
|
||||
{
|
||||
std::cerr << "IMG_Init failed: " << IMG_GetError() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
const bool ok = GenerateDesktopAtlasHeader();
|
||||
IMG_Quit();
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user