很多修改

This commit is contained in:
HP
2026-07-08 15:55:13 +08:00
parent d95ccc0c99
commit 1a194da7ec
32 changed files with 109868 additions and 26 deletions
+101
View File
@@ -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()
+150
View File
@@ -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;
}
+15
View File
@@ -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
+58
View File
@@ -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;
}
}
+38
View File
@@ -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();
};
}
+333
View File
@@ -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];
}
}
+69
View File
@@ -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;
};
}
+25
View File
@@ -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]);
}
}
+20
View File
@@ -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();
}
+135
View File
@@ -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
}
}
+12
View File
@@ -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;
}