47 changed files with 1983 additions and 26 deletions
@ -0,0 +1,31 @@ |
|||||||
|
From: KOS Dreamcast Port
|
||||||
|
Subject: Fix libfmt 12.0.0 long double support on SH4
|
||||||
|
|
||||||
|
On SH4 with -m4-single, long double is IEEE 754 binary64 (same as double)
|
||||||
|
but std::numeric_limits<long double>::digits == 53, which doesn't match
|
||||||
|
fmt's expected values of 64 or 113 for extended precision types.
|
||||||
|
|
||||||
|
This causes the float_info<long double> template to be incomplete,
|
||||||
|
resulting in compilation errors:
|
||||||
|
error: invalid use of incomplete type 'struct float_info<long double>'
|
||||||
|
|
||||||
|
This patch adds an explicit specialization for long double when it has
|
||||||
|
binary64 characteristics (53 mantissa bits, 8 bytes).
|
||||||
|
|
||||||
|
--- a/include/fmt/format.h
|
||||||
|
+++ b/include/fmt/format.h
|
||||||
|
@@ -1462,6 +1462,14 @@ template <> struct float_info<double> {
|
||||||
|
static const int shorter_interval_tie_lower_threshold = -77;
|
||||||
|
static const int shorter_interval_tie_upper_threshold = -77;
|
||||||
|
};
|
||||||
|
+
|
||||||
|
+// SH4/Dreamcast fix: long double is IEEE 754 binary64 on this platform.
|
||||||
|
+// Provide explicit specialization when long double has 53 mantissa bits.
|
||||||
|
+#if defined(__LDBL_MANT_DIG__) && (__LDBL_MANT_DIG__ == 53) && \
|
||||||
|
+ defined(__SIZEOF_LONG_DOUBLE__) && (__SIZEOF_LONG_DOUBLE__ == 8)
|
||||||
|
+template <>
|
||||||
|
+struct float_info<long double> : float_info<double> {};
|
||||||
|
+#endif
|
||||||
|
|
||||||
|
// An 80- or 128-bit floating point number.
|
||||||
|
template <typename T>
|
||||||
@ -0,0 +1,79 @@ |
|||||||
|
# Not automatically defined by sh-elf-gcc |
||||||
|
add_compile_definitions(__DREAMCAST__) |
||||||
|
|
||||||
|
set(CMAKE_BUILD_TYPE MinSizeRel CACHE STRING "" FORCE) |
||||||
|
|
||||||
|
set(ASAN OFF) |
||||||
|
set(UBSAN OFF) |
||||||
|
set(BUILD_TESTING OFF) |
||||||
|
set(NONET ON) |
||||||
|
set(USE_SDL1 ON) |
||||||
|
set(NOEXIT ON) |
||||||
|
set(PREFILL_PLAYER_NAME ON) |
||||||
|
set(DISABLE_DEMOMODE ON) |
||||||
|
set(DEVILUTIONX_SYSTEM_LIBSODIUM OFF) |
||||||
|
set(DEVILUTIONX_SYSTEM_SDL_IMAGE OFF) |
||||||
|
set(UNPACKED_MPQS OFF) |
||||||
|
|
||||||
|
# CD reads via MPQ are too slow for real-time audio |
||||||
|
set(DISABLE_STREAMING_SOUNDS ON) |
||||||
|
set(DEFAULT_AUDIO_BUFFER_SIZE 2048) |
||||||
|
set(DEFAULT_AUDIO_SAMPLE_RATE 22050) |
||||||
|
set(DEFAULT_AUDIO_RESAMPLING_QUALITY 0) |
||||||
|
|
||||||
|
set(DEFAULT_PER_PIXEL_LIGHTING 0) |
||||||
|
set(DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT ON CACHE BOOL "" FORCE) |
||||||
|
|
||||||
|
# GPF SDL supports 8bpp with hardware palette - lets SDL handle the |
||||||
|
# palette-to-RGB conversion internally via PVR DMA, avoiding the |
||||||
|
# manual 8bpp->16bpp conversion in dc_video.cpp entirely. |
||||||
|
set(SDL1_VIDEO_MODE_BPP 8) |
||||||
|
set(SDL1_VIDEO_MODE_FLAGS SDL_DOUBLEBUF|SDL_HWSURFACE) |
||||||
|
|
||||||
|
set(DEFAULT_WIDTH 640) |
||||||
|
set(DEFAULT_HEIGHT 480) |
||||||
|
|
||||||
|
# VMU saves use flat files with zlib compression |
||||||
|
set(UNPACKED_SAVES ON) |
||||||
|
|
||||||
|
set(DEVILUTIONX_GAMEPAD_TYPE Generic) |
||||||
|
|
||||||
|
# GPF SDL button IDs (from SDL_dreamcast.h SDL_DC_button enum) |
||||||
|
set(JOY_BUTTON_A 2) |
||||||
|
set(JOY_BUTTON_B 1) |
||||||
|
set(JOY_BUTTON_X 5) |
||||||
|
set(JOY_BUTTON_Y 6) |
||||||
|
set(JOY_BUTTON_START 3) |
||||||
|
|
||||||
|
set(JOY_HAT_DPAD_UP_HAT 0) |
||||||
|
set(JOY_HAT_DPAD_DOWN_HAT 0) |
||||||
|
set(JOY_HAT_DPAD_LEFT_HAT 0) |
||||||
|
set(JOY_HAT_DPAD_RIGHT_HAT 0) |
||||||
|
set(JOY_HAT_DPAD_UP 1) |
||||||
|
set(JOY_HAT_DPAD_DOWN 4) |
||||||
|
set(JOY_HAT_DPAD_LEFT 8) |
||||||
|
set(JOY_HAT_DPAD_RIGHT 2) |
||||||
|
|
||||||
|
set(JOY_AXIS_LEFTX 0) |
||||||
|
set(JOY_AXIS_LEFTY 1) |
||||||
|
|
||||||
|
list(APPEND DEVILUTIONX_PLATFORM_SUBDIRECTORIES platform/dreamcast) |
||||||
|
list(APPEND DEVILUTIONX_PLATFORM_LINK_LIBRARIES libdevilutionx_dreamcast) |
||||||
|
|
||||||
|
# FindZLIB may not create this target during cross-compilation. |
||||||
|
if(NOT TARGET ZLIB::ZLIB) |
||||||
|
add_library(ZLIB::ZLIB STATIC IMPORTED) |
||||||
|
set_target_properties(ZLIB::ZLIB PROPERTIES |
||||||
|
IMPORTED_LOCATION "${ZLIB_LIBRARY}" |
||||||
|
INTERFACE_INCLUDE_DIRECTORIES "${ZLIB_INCLUDE_DIR}" |
||||||
|
) |
||||||
|
endif() |
||||||
|
|
||||||
|
# FindBZip2 may not create this target during cross-compilation |
||||||
|
if(NOT TARGET BZip2::BZip2) |
||||||
|
add_library(BZip2::BZip2 STATIC IMPORTED) |
||||||
|
set_target_properties(BZip2::BZip2 PROPERTIES |
||||||
|
IMPORTED_LOCATION "${BZIP2_LIBRARIES}" |
||||||
|
INTERFACE_INCLUDE_DIRECTORIES "${BZIP2_INCLUDE_DIR}" |
||||||
|
) |
||||||
|
endif() |
||||||
@ -0,0 +1,67 @@ |
|||||||
|
# DevilutionX Dreamcast Toolchain |
||||||
|
# We extend the official KOS toolchain to add project-specific settings. |
||||||
|
|
||||||
|
if(NOT DEFINED ENV{KOS_CMAKE_TOOLCHAIN}) |
||||||
|
message(FATAL_ERROR "KOS_CMAKE_TOOLCHAIN not defined. Please source environ.sh.") |
||||||
|
endif() |
||||||
|
|
||||||
|
# 1. Load the official KOS toolchain |
||||||
|
# This sets up compilers, flags (-m4-single), and system paths correctly. |
||||||
|
include("$ENV{KOS_CMAKE_TOOLCHAIN}") |
||||||
|
|
||||||
|
# 2. Set DevilutionX specific platform flag |
||||||
|
# This triggers loading CMake/platforms/dreamcast.cmake |
||||||
|
set(DREAMCAST ON) |
||||||
|
|
||||||
|
# 3. Add our include paths |
||||||
|
# KOS ports often hide headers in subdirectories |
||||||
|
# GPF SDL installs to KOS_BASE/addons/include/dreamcast/SDL/ |
||||||
|
list(APPEND CMAKE_INCLUDE_PATH |
||||||
|
"$ENV{KOS_PORTS}/include/zlib" |
||||||
|
"$ENV{KOS_PORTS}/include/bzlib" |
||||||
|
"$ENV{KOS_BASE}/addons/include/dreamcast/SDL" |
||||||
|
) |
||||||
|
|
||||||
|
# 4. Force libraries (The "Nuclear Option" for sub-projects) |
||||||
|
# Standard find modules struggle with KOS layout, so we force them. |
||||||
|
set(ZLIB_INCLUDE_DIR "$ENV{KOS_PORTS}/include/zlib" CACHE PATH "ZLIB Include Dir" FORCE) |
||||||
|
set(ZLIB_LIBRARY "$ENV{KOS_PORTS}/lib/libz.a" CACHE FILEPATH "ZLIB Library" FORCE) |
||||||
|
set(BZIP2_INCLUDE_DIR "$ENV{KOS_PORTS}/include/bzlib" CACHE PATH "BZip2 Include Dir" FORCE) |
||||||
|
set(BZIP2_LIBRARIES "$ENV{KOS_PORTS}/lib/libbz2.a" CACHE FILEPATH "BZip2 Library" FORCE) |
||||||
|
|
||||||
|
# 5. Force SDL1 - GPF SDL (SDL-dreamhal--GLDC) for DMA video and hardware palette |
||||||
|
# GPF SDL installs via its Makefile.dc to KOS_BASE/addons/{lib,include}/dreamcast/ |
||||||
|
set(SDL_INCLUDE_DIR "$ENV{KOS_BASE}/addons/include/dreamcast/SDL" CACHE PATH "SDL Include Dir" FORCE) |
||||||
|
set(SDL_LIBRARY "$ENV{KOS_BASE}/addons/lib/dreamcast/libSDL.a" CACHE FILEPATH "SDL Library" FORCE) |
||||||
|
|
||||||
|
# 6. Fix libfmt and magic_enum compilation |
||||||
|
# Disable long double support because sh4-gcc's long double (64-bit) confuses libfmt |
||||||
|
# Disable magic_enum asserts because GCC 15's stricter parsing breaks them in comma expressions |
||||||
|
add_compile_definitions(FMT_USE_LONG_DOUBLE=0 FMT_USE_FLOAT128=0 MAGIC_ENUM_NO_ASSERT) |
||||||
|
|
||||||
|
# 7. KOS doesn't have pthreads as a separate library - threading is built into libkallisti |
||||||
|
set(CMAKE_THREAD_LIBS_INIT "" CACHE STRING "" FORCE) |
||||||
|
set(CMAKE_HAVE_THREADS_LIBRARY 1) |
||||||
|
set(Threads_FOUND TRUE) |
||||||
|
|
||||||
|
# 8. Remove host system library paths that sneak in |
||||||
|
set(CMAKE_EXE_LINKER_FLAGS "" CACHE STRING "" FORCE) |
||||||
|
set(CMAKE_SHARED_LINKER_FLAGS "" CACHE STRING "" FORCE) |
||||||
|
|
||||||
|
# 8b. Ignore host system paths (homebrew, etc.) to prevent finding wrong libraries |
||||||
|
set(CMAKE_IGNORE_PATH "/opt/homebrew;/opt/homebrew/include;/opt/homebrew/lib;/usr/local;/usr/local/include;/usr/local/lib" CACHE STRING "" FORCE) |
||||||
|
|
||||||
|
# 8c. Force using bundled magic_enum (not system/homebrew version) |
||||||
|
set(DEVILUTIONX_SYSTEM_MAGIC_ENUM OFF CACHE BOOL "" FORCE) |
||||||
|
|
||||||
|
# 9. Override Link Rule to use Grouping |
||||||
|
# KOS toolchain already adds -T, -nodefaultlibs, and -L paths via <LINK_FLAGS> |
||||||
|
# We just need to add --start-group/--end-group for circular dependency resolution |
||||||
|
# NOTE: kos_add_romdisk() in CMakeLists.txt handles -lromdiskbase with --whole-archive |
||||||
|
set(CMAKE_CXX_LINK_EXECUTABLE |
||||||
|
"<CMAKE_CXX_COMPILER> <FLAGS> <LINK_FLAGS> <OBJECTS> -o <TARGET> -Wl,--start-group <LINK_LIBRARIES> -lkallisti -lc -lgcc -lm -lstdc++ -Wl,--end-group") |
||||||
|
set(CMAKE_C_LINK_EXECUTABLE |
||||||
|
"<CMAKE_C_COMPILER> <FLAGS> <LINK_FLAGS> <OBJECTS> -o <TARGET> -Wl,--start-group <LINK_LIBRARIES> -lkallisti -lc -lgcc -lm -lstdc++ -Wl,--end-group") |
||||||
|
|
||||||
|
# 10. Skip compiler checks (saves time and avoids romdisk symbol issues) |
||||||
|
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) |
||||||
@ -0,0 +1,50 @@ |
|||||||
|
# Build artifacts |
||||||
|
*.iso |
||||||
|
*.cdi |
||||||
|
*.bin |
||||||
|
|
||||||
|
# Disc staging directories |
||||||
|
cd_root/ |
||||||
|
flycast_disc/ |
||||||
|
romdisk/ |
||||||
|
objects/ |
||||||
|
towners/ |
||||||
|
items/ |
||||||
|
levels/ |
||||||
|
monsters/ |
||||||
|
music/ |
||||||
|
nlevels/ |
||||||
|
plrgfx/ |
||||||
|
sfx/ |
||||||
|
speech/ |
||||||
|
ui_art/ |
||||||
|
data/ |
||||||
|
fonts/ |
||||||
|
gendata/ |
||||||
|
arena/ |
||||||
|
|
||||||
|
# Extracted game data (copyrighted) |
||||||
|
diabdat/ |
||||||
|
DIABDAT.MPQ |
||||||
|
spawn.mpq |
||||||
|
|
||||||
|
# Extracted file dumps (from MPQ tools) |
||||||
|
File[0-9]*.pcx |
||||||
|
File[0-9]*.wav |
||||||
|
File[0-9]*.xxx |
||||||
|
File[0-9]*.gif |
||||||
|
File[0-9]*.smk |
||||||
|
|
||||||
|
# GPF SDL source (cloned by build.sh) |
||||||
|
SDL-gpf/ |
||||||
|
|
||||||
|
# Build tool output |
||||||
|
*.o |
||||||
|
|
||||||
|
# Listfiles (generated) |
||||||
|
*_listfile*.txt |
||||||
|
|
||||||
|
# Python / IDE |
||||||
|
.venv/ |
||||||
|
.qodo/ |
||||||
|
__pycache__/ |
||||||
@ -0,0 +1,39 @@ |
|||||||
|
# Dreamcast Build |
||||||
|
|
||||||
|
This folder contains the Dreamcast packaging flow for DevilutionX. |
||||||
|
|
||||||
|
## Prerequisites |
||||||
|
|
||||||
|
- [KallistiOS](https://kos-docs.dreamcast.wiki/) (KOS) with kos-ports (zlib, bzip2) |
||||||
|
- [`mkdcdisc`](https://gitlab.com/simulant/mkdcdisc) for CDI disc image creation |
||||||
|
|
||||||
|
SDL (GPF SDL with DMA video) and Lua are built from source automatically by `build.sh`. |
||||||
|
No external `fmt` patch is required. The build applies a bundled SH4 `libfmt` patch automatically. |
||||||
|
|
||||||
|
## Game Data (Required) |
||||||
|
|
||||||
|
- You must provide your own MPQ files. Diablo data files are proprietary assets owned by Blizzard. |
||||||
|
- Copy `DIABDAT.MPQ` to `Packaging/dreamcast/cd_root/`. |
||||||
|
- `spawn.mpq` is optional for shareware mode. |
||||||
|
- For extraction methods, see [Extracting MPQs from the GoG installer](https://github.com/diasurgical/devilutionX/wiki/Extracting-MPQs-from-the-GoG-installer). |
||||||
|
|
||||||
|
## Build |
||||||
|
|
||||||
|
From repo root: |
||||||
|
|
||||||
|
```sh |
||||||
|
./Packaging/dreamcast/build.sh |
||||||
|
``` |
||||||
|
|
||||||
|
## Output |
||||||
|
|
||||||
|
- Intermediate ELF: `build-dreamcast/devilutionx.elf` |
||||||
|
- Bootable CDI: `Packaging/dreamcast/devilutionx-playable.cdi` |
||||||
|
|
||||||
|
## Notes |
||||||
|
|
||||||
|
- `build.sh` deletes and recreates `build-dreamcast/`. |
||||||
|
- Override tool paths with env vars if needed: |
||||||
|
- `KOS_BASE` |
||||||
|
- `KOS_ENV` |
||||||
|
- `MKDCDISC` |
||||||
@ -0,0 +1,82 @@ |
|||||||
|
#!/bin/bash |
||||||
|
set -euo pipefail |
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
||||||
|
ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)" |
||||||
|
BUILD_DIR="${ROOT_DIR}/build-dreamcast" |
||||||
|
CD_ROOT="${SCRIPT_DIR}/cd_root" |
||||||
|
|
||||||
|
ELF_NAME="devilutionx.elf" |
||||||
|
STRIPPED_NAME="devilutionx-stripped.elf" |
||||||
|
BIN_NAME="1ST_READ.BIN" |
||||||
|
|
||||||
|
KOS_BASE="${KOS_BASE:-/opt/toolchains/dc/kos}" |
||||||
|
KOS_ENV="${KOS_ENV:-${KOS_BASE}/environ.sh}" |
||||||
|
MKDCDISC="${MKDCDISC:-/opt/toolchains/dc/antiruins/tools/mkdcdisc}" |
||||||
|
|
||||||
|
if [ -f "${KOS_ENV}" ]; then |
||||||
|
# shellcheck disable=SC1090 |
||||||
|
# Temporarily disable -u because KOS environ may have unbound variables |
||||||
|
set +u |
||||||
|
source "${KOS_ENV}" |
||||||
|
set -u |
||||||
|
else |
||||||
|
echo "Error: KOS environ not found at ${KOS_ENV}" |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
|
||||||
|
if [ ! -x "${MKDCDISC}" ]; then |
||||||
|
echo "Error: mkdcdisc not found at ${MKDCDISC}" |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
|
||||||
|
GPF_SDL_DIR="${SCRIPT_DIR}/SDL-gpf" |
||||||
|
GPF_SDL_LIB="${KOS_BASE}/addons/lib/dreamcast/libSDL.a" |
||||||
|
GPF_SDL_HEADER="${KOS_BASE}/addons/include/dreamcast/SDL/SDL_dreamcast.h" |
||||||
|
|
||||||
|
if [ ! -f "${GPF_SDL_HEADER}" ]; then |
||||||
|
echo "Building GPF SDL (SDL-dreamhal--GLDC) for DMA video..." |
||||||
|
if [ ! -d "${GPF_SDL_DIR}" ]; then |
||||||
|
git clone --depth 1 -b SDL-dreamhal--GLDC \ |
||||||
|
https://github.com/GPF/SDL-1.2 "${GPF_SDL_DIR}" |
||||||
|
fi |
||||||
|
make -C "${GPF_SDL_DIR}" -f Makefile.dc -j"$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)" |
||||||
|
echo "GPF SDL installed to ${GPF_SDL_LIB}" |
||||||
|
else |
||||||
|
echo "GPF SDL already installed at ${GPF_SDL_LIB}" |
||||||
|
fi |
||||||
|
|
||||||
|
rm -rf "${BUILD_DIR}" |
||||||
|
mkdir -p "${BUILD_DIR}" |
||||||
|
|
||||||
|
cmake_args=( |
||||||
|
-S "${ROOT_DIR}" |
||||||
|
-B "${BUILD_DIR}" |
||||||
|
-DCMAKE_TOOLCHAIN_FILE="${ROOT_DIR}/CMake/platforms/dreamcast.toolchain.cmake" |
||||||
|
-DDEVILUTIONX_SYSTEM_LUA=OFF |
||||||
|
) |
||||||
|
|
||||||
|
if command -v ninja >/dev/null 2>&1; then |
||||||
|
cmake_args+=(-G Ninja) |
||||||
|
fi |
||||||
|
|
||||||
|
cmake "${cmake_args[@]}" |
||||||
|
cmake --build "${BUILD_DIR}" -j"$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)" |
||||||
|
|
||||||
|
if [ ! -f "${BUILD_DIR}/${ELF_NAME}" ]; then |
||||||
|
echo "Error: ${BUILD_DIR}/${ELF_NAME} not found after build" |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
|
||||||
|
sh-elf-strip -s "${BUILD_DIR}/${ELF_NAME}" -o "${BUILD_DIR}/${STRIPPED_NAME}" |
||||||
|
|
||||||
|
mkdir -p "${CD_ROOT}" |
||||||
|
cp "${BUILD_DIR}/${STRIPPED_NAME}" "${CD_ROOT}/${BIN_NAME}" |
||||||
|
cp "${BUILD_DIR}/devilutionx.mpq" "${CD_ROOT}/devilutionx.mpq" |
||||||
|
|
||||||
|
"${MKDCDISC}" -e "${CD_ROOT}/${BIN_NAME}" \ |
||||||
|
-D "${CD_ROOT}" \ |
||||||
|
-o "${SCRIPT_DIR}/devilutionx-playable.cdi" \ |
||||||
|
-n "DevilutionX" |
||||||
|
|
||||||
|
echo "CDI: ${SCRIPT_DIR}/devilutionx-playable.cdi" |
||||||
@ -0,0 +1,13 @@ |
|||||||
|
include(functions/devilutionx_library) |
||||||
|
|
||||||
|
add_devilutionx_object_library(libdevilutionx_dreamcast |
||||||
|
posix_stubs.c |
||||||
|
dc_video.cpp |
||||||
|
dc_init.cpp |
||||||
|
dc_save_codec.cpp |
||||||
|
) |
||||||
|
|
||||||
|
target_link_libraries(libdevilutionx_dreamcast PUBLIC |
||||||
|
DevilutionX::SDL |
||||||
|
ZLIB::ZLIB |
||||||
|
) |
||||||
@ -0,0 +1,59 @@ |
|||||||
|
#ifdef __DREAMCAST__ |
||||||
|
|
||||||
|
#include "dc_init.hpp" |
||||||
|
#include "dc_video.h" |
||||||
|
#include "utils/paths.h" |
||||||
|
#include <cstdio> |
||||||
|
#include <dc/maple.h> |
||||||
|
#include <dc/maple/vmu.h> |
||||||
|
#include <kos.h> |
||||||
|
|
||||||
|
namespace devilution { |
||||||
|
namespace dc { |
||||||
|
|
||||||
|
static bool g_vmuAvailable = false; |
||||||
|
static char g_vmuPath[32] = "/vmu/a1/"; |
||||||
|
|
||||||
|
static bool CheckVmuAvailable() |
||||||
|
{ |
||||||
|
maple_device_t *vmu = maple_enum_type(0, MAPLE_FUNC_MEMCARD); |
||||||
|
if (vmu) { |
||||||
|
snprintf(g_vmuPath, sizeof(g_vmuPath), "/vmu/%c%d/", |
||||||
|
'a' + vmu->port, vmu->unit); |
||||||
|
g_vmuAvailable = true; |
||||||
|
return true; |
||||||
|
} |
||||||
|
g_vmuAvailable = false; |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
bool IsVmuAvailable() |
||||||
|
{ |
||||||
|
return g_vmuAvailable; |
||||||
|
} |
||||||
|
|
||||||
|
const char *GetVmuPath() |
||||||
|
{ |
||||||
|
return g_vmuPath; |
||||||
|
} |
||||||
|
|
||||||
|
bool InitDreamcast() |
||||||
|
{ |
||||||
|
paths::SetBasePath("/cd/"); |
||||||
|
CheckVmuAvailable(); |
||||||
|
// Saves use /ram/ for fast in-session access.
|
||||||
|
// Save entries are mirrored to VMU and restored on demand at load time.
|
||||||
|
paths::SetPrefPath("/ram/"); |
||||||
|
paths::SetConfigPath("/ram/"); |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
void ShutdownDreamcast() |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace dc
|
||||||
|
} // namespace devilution
|
||||||
|
|
||||||
|
#endif |
||||||
@ -0,0 +1,44 @@ |
|||||||
|
/**
|
||||||
|
* @file dc_init.hpp |
||||||
|
* @brief Dreamcast-specific initialization API |
||||||
|
*/ |
||||||
|
#pragma once |
||||||
|
|
||||||
|
#ifdef __DREAMCAST__ |
||||||
|
|
||||||
|
namespace devilution { |
||||||
|
namespace dc { |
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Dreamcast subsystems (video, VMU, etc.) |
||||||
|
* Call from main() before game initialization. |
||||||
|
* @return true on success |
||||||
|
*/ |
||||||
|
bool InitDreamcast(); |
||||||
|
|
||||||
|
/**
|
||||||
|
* Shutdown Dreamcast subsystems. |
||||||
|
* Call before program exit. |
||||||
|
*/ |
||||||
|
void ShutdownDreamcast(); |
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if VMU (Visual Memory Unit) is available for saves. |
||||||
|
* VMU is the Dreamcast's memory card, accessed via /vmu/[port][unit]/ |
||||||
|
* |
||||||
|
* @return true if a VMU was detected |
||||||
|
*/ |
||||||
|
bool IsVmuAvailable(); |
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the VMU filesystem path (e.g., "/vmu/a1/") |
||||||
|
* Only valid if IsVmuAvailable() returns true. |
||||||
|
* |
||||||
|
* @return Path string to VMU root |
||||||
|
*/ |
||||||
|
const char *GetVmuPath(); |
||||||
|
|
||||||
|
} // namespace dc
|
||||||
|
} // namespace devilution
|
||||||
|
|
||||||
|
#endif // __DREAMCAST__
|
||||||
@ -0,0 +1,381 @@ |
|||||||
|
/**
|
||||||
|
* @file dc_save_codec.cpp |
||||||
|
* @brief Dreamcast save compression with zlib |
||||||
|
*/ |
||||||
|
|
||||||
|
#ifdef __DREAMCAST__ |
||||||
|
|
||||||
|
#include "dc_save_codec.hpp" |
||||||
|
|
||||||
|
#include "utils/log.hpp" |
||||||
|
|
||||||
|
#include <algorithm> |
||||||
|
#include <cstdint> |
||||||
|
#include <cstdio> |
||||||
|
#include <cstring> |
||||||
|
#include <limits> |
||||||
|
#include <string> |
||||||
|
#include <zlib.h> |
||||||
|
|
||||||
|
#include <dc/fs_vmu.h> |
||||||
|
#include <dc/vmu_pkg.h> |
||||||
|
#include <kos/fs.h> |
||||||
|
|
||||||
|
namespace devilution { |
||||||
|
namespace dc { |
||||||
|
|
||||||
|
namespace { |
||||||
|
|
||||||
|
constexpr size_t MaxSaveDataSize = 512 * 1024; |
||||||
|
constexpr size_t SaveHeaderSize = 16; |
||||||
|
constexpr uint8_t SaveFormatVersion = 1; |
||||||
|
constexpr char SaveMagic[4] = { 'D', 'X', 'Z', '1' }; |
||||||
|
|
||||||
|
enum class SaveCodec : uint8_t { |
||||||
|
Raw = 0, |
||||||
|
Zlib = 1, |
||||||
|
}; |
||||||
|
|
||||||
|
uint32_t ReadU32LE(const std::byte *data) |
||||||
|
{ |
||||||
|
const auto *bytes = reinterpret_cast<const uint8_t *>(data); |
||||||
|
return static_cast<uint32_t>(bytes[0]) |
||||||
|
| (static_cast<uint32_t>(bytes[1]) << 8) |
||||||
|
| (static_cast<uint32_t>(bytes[2]) << 16) |
||||||
|
| (static_cast<uint32_t>(bytes[3]) << 24); |
||||||
|
} |
||||||
|
|
||||||
|
void WriteU32LE(std::byte *destination, uint32_t value) |
||||||
|
{ |
||||||
|
auto *bytes = reinterpret_cast<uint8_t *>(destination); |
||||||
|
bytes[0] = static_cast<uint8_t>(value & 0xFF); |
||||||
|
bytes[1] = static_cast<uint8_t>((value >> 8) & 0xFF); |
||||||
|
bytes[2] = static_cast<uint8_t>((value >> 16) & 0xFF); |
||||||
|
bytes[3] = static_cast<uint8_t>((value >> 24) & 0xFF); |
||||||
|
} |
||||||
|
|
||||||
|
bool IsSaveContainer(const std::byte *data, size_t size) |
||||||
|
{ |
||||||
|
if (size < SaveHeaderSize) |
||||||
|
return false; |
||||||
|
|
||||||
|
const auto *bytes = reinterpret_cast<const char *>(data); |
||||||
|
return bytes[0] == SaveMagic[0] |
||||||
|
&& bytes[1] == SaveMagic[1] |
||||||
|
&& bytes[2] == SaveMagic[2] |
||||||
|
&& bytes[3] == SaveMagic[3] |
||||||
|
&& static_cast<uint8_t>(bytes[4]) == SaveFormatVersion; |
||||||
|
} |
||||||
|
|
||||||
|
bool DecodeZlib( |
||||||
|
const std::byte *input, |
||||||
|
size_t inputSize, |
||||||
|
std::byte *output, |
||||||
|
size_t outputCapacity, |
||||||
|
size_t &decodedSize) |
||||||
|
{ |
||||||
|
if (input == nullptr || output == nullptr || outputCapacity == 0) |
||||||
|
return false; |
||||||
|
if (inputSize > static_cast<size_t>(std::numeric_limits<uLong>::max())) |
||||||
|
return false; |
||||||
|
if (outputCapacity > static_cast<size_t>(std::numeric_limits<uLong>::max())) |
||||||
|
return false; |
||||||
|
|
||||||
|
uLongf size = static_cast<uLongf>(outputCapacity); |
||||||
|
const int rc = uncompress( |
||||||
|
reinterpret_cast<Bytef *>(output), |
||||||
|
&size, |
||||||
|
reinterpret_cast<const Bytef *>(input), |
||||||
|
static_cast<uLong>(inputSize)); |
||||||
|
if (rc != Z_OK) |
||||||
|
return false; |
||||||
|
if (size != outputCapacity) |
||||||
|
return false; |
||||||
|
|
||||||
|
decodedSize = static_cast<size_t>(size); |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
std::unique_ptr<std::byte[]> BuildSaveContainer(const std::byte *data, size_t size, size_t &outSize) |
||||||
|
{ |
||||||
|
outSize = 0; |
||||||
|
if (data == nullptr || size == 0 || size > MaxSaveDataSize) |
||||||
|
return nullptr; |
||||||
|
if (size > static_cast<size_t>(std::numeric_limits<uLong>::max())) |
||||||
|
return nullptr; |
||||||
|
|
||||||
|
SaveCodec codec = SaveCodec::Raw; |
||||||
|
size_t payloadSize = size; |
||||||
|
std::unique_ptr<std::byte[]> compressed; |
||||||
|
uLongf compressedSize = compressBound(static_cast<uLong>(size)); |
||||||
|
|
||||||
|
if (compressedSize > 0) { |
||||||
|
compressed = std::make_unique<std::byte[]>(compressedSize); |
||||||
|
const int rc = compress2( |
||||||
|
reinterpret_cast<Bytef *>(compressed.get()), |
||||||
|
&compressedSize, |
||||||
|
reinterpret_cast<const Bytef *>(data), |
||||||
|
static_cast<uLong>(size), |
||||||
|
Z_BEST_SPEED); |
||||||
|
if (rc == Z_OK && compressedSize < size) { |
||||||
|
codec = SaveCodec::Zlib; |
||||||
|
payloadSize = static_cast<size_t>(compressedSize); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (payloadSize > std::numeric_limits<uint32_t>::max()) |
||||||
|
return nullptr; |
||||||
|
if (size > std::numeric_limits<uint32_t>::max()) |
||||||
|
return nullptr; |
||||||
|
|
||||||
|
const size_t containerSize = SaveHeaderSize + payloadSize; |
||||||
|
auto container = std::make_unique<std::byte[]>(containerSize); |
||||||
|
|
||||||
|
auto *bytes = reinterpret_cast<char *>(container.get()); |
||||||
|
bytes[0] = SaveMagic[0]; |
||||||
|
bytes[1] = SaveMagic[1]; |
||||||
|
bytes[2] = SaveMagic[2]; |
||||||
|
bytes[3] = SaveMagic[3]; |
||||||
|
bytes[4] = static_cast<char>(SaveFormatVersion); |
||||||
|
bytes[5] = static_cast<char>(codec); |
||||||
|
bytes[6] = 0; |
||||||
|
bytes[7] = 0; |
||||||
|
WriteU32LE(container.get() + 8, static_cast<uint32_t>(payloadSize)); |
||||||
|
WriteU32LE(container.get() + 12, static_cast<uint32_t>(size)); |
||||||
|
|
||||||
|
if (codec == SaveCodec::Zlib) { |
||||||
|
std::memcpy(container.get() + SaveHeaderSize, compressed.get(), payloadSize); |
||||||
|
LogVerbose("[DC Save] zlib compressed {} -> {} bytes ({:.1f}%)", |
||||||
|
size, payloadSize, 100.0f * payloadSize / size); |
||||||
|
} else { |
||||||
|
std::memcpy(container.get() + SaveHeaderSize, data, payloadSize); |
||||||
|
LogVerbose("[DC Save] Stored {} bytes as raw payload", size); |
||||||
|
} |
||||||
|
|
||||||
|
outSize = containerSize; |
||||||
|
return container; |
||||||
|
} |
||||||
|
|
||||||
|
std::unique_ptr<std::byte[]> DecodeSaveContainer( |
||||||
|
const std::byte *data, |
||||||
|
size_t size, |
||||||
|
size_t &outSize, |
||||||
|
const char *sourceTag) |
||||||
|
{ |
||||||
|
outSize = 0; |
||||||
|
if (!IsSaveContainer(data, size)) { |
||||||
|
LogError("[DC Save] Invalid save container format in {}", sourceTag); |
||||||
|
return nullptr; |
||||||
|
} |
||||||
|
|
||||||
|
const uint8_t codec = static_cast<uint8_t>(reinterpret_cast<const char *>(data)[5]); |
||||||
|
const uint32_t payloadSize = ReadU32LE(data + 8); |
||||||
|
const uint32_t originalSize = ReadU32LE(data + 12); |
||||||
|
|
||||||
|
if (originalSize == 0 || originalSize > MaxSaveDataSize) { |
||||||
|
LogError("[DC Save] Invalid container original size {} in {}", originalSize, sourceTag); |
||||||
|
return nullptr; |
||||||
|
} |
||||||
|
if (payloadSize > size - SaveHeaderSize) { |
||||||
|
LogError("[DC Save] Invalid container payload size {} in {}", payloadSize, sourceTag); |
||||||
|
return nullptr; |
||||||
|
} |
||||||
|
|
||||||
|
const std::byte *payload = data + SaveHeaderSize; |
||||||
|
auto decoded = std::make_unique<std::byte[]>(originalSize); |
||||||
|
|
||||||
|
if (codec == static_cast<uint8_t>(SaveCodec::Raw)) { |
||||||
|
if (payloadSize < originalSize) { |
||||||
|
LogError("[DC Save] Raw payload too small in {}", sourceTag); |
||||||
|
return nullptr; |
||||||
|
} |
||||||
|
std::memcpy(decoded.get(), payload, originalSize); |
||||||
|
outSize = originalSize; |
||||||
|
return decoded; |
||||||
|
} |
||||||
|
|
||||||
|
if (codec == static_cast<uint8_t>(SaveCodec::Zlib)) { |
||||||
|
size_t decodedSize = 0; |
||||||
|
if (!DecodeZlib(payload, payloadSize, decoded.get(), originalSize, decodedSize)) { |
||||||
|
LogError("[DC Save] zlib decode failed for {}", sourceTag); |
||||||
|
return nullptr; |
||||||
|
} |
||||||
|
outSize = decodedSize; |
||||||
|
return decoded; |
||||||
|
} |
||||||
|
|
||||||
|
LogError("[DC Save] Unknown container codec {} in {}", codec, sourceTag); |
||||||
|
return nullptr; |
||||||
|
} |
||||||
|
|
||||||
|
bool ReadFileBytes(const char *path, std::unique_ptr<std::byte[]> &buffer, size_t &size) |
||||||
|
{ |
||||||
|
buffer.reset(); |
||||||
|
size = 0; |
||||||
|
|
||||||
|
FILE *file = std::fopen(path, "rb"); |
||||||
|
if (file == nullptr) |
||||||
|
return false; |
||||||
|
|
||||||
|
if (std::fseek(file, 0, SEEK_END) != 0) { |
||||||
|
std::fclose(file); |
||||||
|
return false; |
||||||
|
} |
||||||
|
const long fileLen = std::ftell(file); |
||||||
|
if (fileLen <= 0) { |
||||||
|
std::fclose(file); |
||||||
|
return false; |
||||||
|
} |
||||||
|
if (std::fseek(file, 0, SEEK_SET) != 0) { |
||||||
|
std::fclose(file); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
size = static_cast<size_t>(fileLen); |
||||||
|
buffer = std::make_unique<std::byte[]>(size); |
||||||
|
if (std::fread(buffer.get(), size, 1, file) != 1) { |
||||||
|
std::fclose(file); |
||||||
|
buffer.reset(); |
||||||
|
size = 0; |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
std::fclose(file); |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool WriteCompressedFile(const char *path, const std::byte *data, size_t size) |
||||||
|
{ |
||||||
|
if (data == nullptr || size == 0 || size > MaxSaveDataSize || size > std::numeric_limits<uint32_t>::max()) { |
||||||
|
LogError("[DC Save] Refusing to write invalid save payload ({} bytes) to {}", size, path); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
size_t containerSize = 0; |
||||||
|
auto container = BuildSaveContainer(data, size, containerSize); |
||||||
|
if (!container || containerSize == 0) { |
||||||
|
LogError("[DC Save] Failed to create save container for {}", path); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
FILE *file = std::fopen(path, "wb"); |
||||||
|
if (file == nullptr) { |
||||||
|
LogError("[DC Save] Failed to open {} for writing", path); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
if (std::fwrite(container.get(), containerSize, 1, file) != 1) { |
||||||
|
LogError("[DC Save] Failed to write container data to {}", path); |
||||||
|
std::fclose(file); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
std::fclose(file); |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
std::unique_ptr<std::byte[]> ReadCompressedFile(const char *path, size_t &outSize) |
||||||
|
{ |
||||||
|
std::unique_ptr<std::byte[]> fileBytes; |
||||||
|
size_t fileSize = 0; |
||||||
|
if (!ReadFileBytes(path, fileBytes, fileSize)) { |
||||||
|
outSize = 0; |
||||||
|
return nullptr; |
||||||
|
} |
||||||
|
return DecodeSaveContainer(fileBytes.get(), fileSize, outSize, path); |
||||||
|
} |
||||||
|
|
||||||
|
// Blank 32x32 icon (4bpp = 512 bytes) for VMU file display.
|
||||||
|
static uint8_t g_blankIcon[512] = { 0 }; |
||||||
|
|
||||||
|
bool WriteToVmu(const char *vmuPath, const char *filename, |
||||||
|
const std::byte *data, size_t size) |
||||||
|
{ |
||||||
|
if (data == nullptr || size == 0 || size > MaxSaveDataSize) |
||||||
|
return false; |
||||||
|
|
||||||
|
size_t payloadSize = 0; |
||||||
|
auto payload = BuildSaveContainer(data, size, payloadSize); |
||||||
|
if (!payload || payloadSize == 0) { |
||||||
|
LogError("[DC VMU] Failed to create save container for {}", filename); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
vmu_pkg_t pkg; |
||||||
|
std::memset(&pkg, 0, sizeof(pkg)); |
||||||
|
std::strncpy(pkg.desc_short, "DevilutionX", sizeof(pkg.desc_short) - 1); |
||||||
|
std::strncpy(pkg.desc_long, "DevilutionX Save Data", sizeof(pkg.desc_long) - 1); |
||||||
|
std::strncpy(pkg.app_id, "DevilutionX", sizeof(pkg.app_id) - 1); |
||||||
|
pkg.icon_cnt = 1; |
||||||
|
pkg.icon_anim_speed = 0; |
||||||
|
pkg.icon_data = g_blankIcon; |
||||||
|
pkg.eyecatch_type = VMUPKG_EC_NONE; |
||||||
|
|
||||||
|
const std::string fullPath = std::string(vmuPath) + filename; |
||||||
|
fs_unlink(fullPath.c_str()); |
||||||
|
|
||||||
|
file_t fd = fs_open(fullPath.c_str(), O_WRONLY); |
||||||
|
if (fd == FILEHND_INVALID) { |
||||||
|
LogError("[DC VMU] Cannot open {} for writing", fullPath); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
fs_vmu_set_header(fd, &pkg); |
||||||
|
const ssize_t written = fs_write(fd, payload.get(), payloadSize); |
||||||
|
const int closeRet = fs_close(fd); |
||||||
|
|
||||||
|
if (written < 0 || written != static_cast<ssize_t>(payloadSize)) { |
||||||
|
LogError("[DC VMU] Short write to {}: {} of {}", fullPath, written, payloadSize); |
||||||
|
return false; |
||||||
|
} |
||||||
|
if (closeRet < 0) { |
||||||
|
LogError("[DC VMU] Close failed for {} (VMU full?)", fullPath); |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
LogVerbose("[DC VMU] Saved {} ({} -> {} bytes on VMU)", fullPath, size, payloadSize); |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
std::unique_ptr<std::byte[]> ReadFromVmu(const char *vmuPath, const char *filename, |
||||||
|
size_t &outSize) |
||||||
|
{ |
||||||
|
outSize = 0; |
||||||
|
const std::string fullPath = std::string(vmuPath) + filename; |
||||||
|
|
||||||
|
file_t fd = fs_open(fullPath.c_str(), O_RDONLY); |
||||||
|
if (fd == FILEHND_INVALID) |
||||||
|
return nullptr; |
||||||
|
|
||||||
|
size_t total = fs_total(fd); |
||||||
|
if (total == static_cast<size_t>(-1) || total < sizeof(uint32_t)) { |
||||||
|
fs_close(fd); |
||||||
|
return nullptr; |
||||||
|
} |
||||||
|
|
||||||
|
auto rawBuf = std::make_unique<std::byte[]>(total); |
||||||
|
const ssize_t bytesRead = fs_read(fd, rawBuf.get(), total); |
||||||
|
fs_close(fd); |
||||||
|
|
||||||
|
if (bytesRead < static_cast<ssize_t>(sizeof(uint32_t))) |
||||||
|
return nullptr; |
||||||
|
|
||||||
|
return DecodeSaveContainer(rawBuf.get(), static_cast<size_t>(bytesRead), outSize, fullPath.c_str()); |
||||||
|
} |
||||||
|
|
||||||
|
bool VmuFileExists(const char *vmuPath, const char *filename) |
||||||
|
{ |
||||||
|
const std::string fullPath = std::string(vmuPath) + filename; |
||||||
|
file_t fd = fs_open(fullPath.c_str(), O_RDONLY); |
||||||
|
if (fd == FILEHND_INVALID) |
||||||
|
return false; |
||||||
|
fs_close(fd); |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace dc
|
||||||
|
} // namespace devilution
|
||||||
|
|
||||||
|
#endif // __DREAMCAST__
|
||||||
@ -0,0 +1,31 @@ |
|||||||
|
/**
|
||||||
|
* @file dc_save_codec.hpp |
||||||
|
* @brief Dreamcast save container helpers |
||||||
|
* |
||||||
|
* Save reads and writes use the zlib-based container format. |
||||||
|
*/ |
||||||
|
|
||||||
|
#pragma once |
||||||
|
|
||||||
|
#ifdef __DREAMCAST__ |
||||||
|
|
||||||
|
#include <cstddef> |
||||||
|
#include <memory> |
||||||
|
|
||||||
|
namespace devilution { |
||||||
|
namespace dc { |
||||||
|
|
||||||
|
bool WriteCompressedFile(const char *path, const std::byte *data, size_t size); |
||||||
|
std::unique_ptr<std::byte[]> ReadCompressedFile(const char *path, size_t &outSize); |
||||||
|
|
||||||
|
bool WriteToVmu(const char *vmuPath, const char *filename, |
||||||
|
const std::byte *data, size_t size); |
||||||
|
std::unique_ptr<std::byte[]> ReadFromVmu(const char *vmuPath, const char *filename, |
||||||
|
size_t &outSize); |
||||||
|
|
||||||
|
bool VmuFileExists(const char *vmuPath, const char *filename); |
||||||
|
|
||||||
|
} // namespace dc
|
||||||
|
} // namespace devilution
|
||||||
|
|
||||||
|
#endif // __DREAMCAST__
|
||||||
@ -0,0 +1,182 @@ |
|||||||
|
/**
|
||||||
|
* @file dc_video.cpp |
||||||
|
* @brief Dreamcast video conversion implementation |
||||||
|
* |
||||||
|
* The "Inner Loop" - this code runs 307,200 times per frame (640x480). |
||||||
|
* Every cycle counts! |
||||||
|
* |
||||||
|
* Optimization techniques used: |
||||||
|
* 1. Palette LUT fits in L1 cache (512 bytes for 256 RGB565 entries) |
||||||
|
* 2. Process 16 pixels at a time for throughput |
||||||
|
*/ |
||||||
|
|
||||||
|
#ifdef __DREAMCAST__ |
||||||
|
|
||||||
|
#include "dc_video.h" |
||||||
|
|
||||||
|
#include <kos.h> |
||||||
|
|
||||||
|
namespace devilution { |
||||||
|
namespace dc { |
||||||
|
|
||||||
|
namespace { |
||||||
|
|
||||||
|
// RGB565 palette lookup table (256 entries x 2 bytes = 512 bytes)
|
||||||
|
// Aligned to 32 bytes for cache efficiency
|
||||||
|
alignas(32) uint16_t palette565[256]; |
||||||
|
// 32-bit word lookup tables used by the packed conversion path.
|
||||||
|
alignas(32) uint32_t palette565FirstWord[256]; |
||||||
|
alignas(32) uint32_t palette565SecondWord[256]; |
||||||
|
|
||||||
|
bool initialized = false; |
||||||
|
|
||||||
|
inline void UpdatePaletteEntry(int index, uint16_t rgb565) |
||||||
|
{ |
||||||
|
palette565[index] = rgb565; |
||||||
|
#if SDL_BYTEORDER == SDL_LIL_ENDIAN |
||||||
|
palette565FirstWord[index] = rgb565; |
||||||
|
palette565SecondWord[index] = static_cast<uint32_t>(rgb565) << 16; |
||||||
|
#else |
||||||
|
palette565FirstWord[index] = static_cast<uint32_t>(rgb565) << 16; |
||||||
|
palette565SecondWord[index] = rgb565; |
||||||
|
#endif |
||||||
|
} |
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Convert 16 pixels from 8bpp to 16bpp |
||||||
|
* |
||||||
|
* This is the innermost loop - fully unrolled for speed. |
||||||
|
*/ |
||||||
|
inline void Convert16PixelsScalar(const uint8_t *src, uint16_t *dst) |
||||||
|
{ |
||||||
|
dst[0] = palette565[src[0]]; |
||||||
|
dst[1] = palette565[src[1]]; |
||||||
|
dst[2] = palette565[src[2]]; |
||||||
|
dst[3] = palette565[src[3]]; |
||||||
|
dst[4] = palette565[src[4]]; |
||||||
|
dst[5] = palette565[src[5]]; |
||||||
|
dst[6] = palette565[src[6]]; |
||||||
|
dst[7] = palette565[src[7]]; |
||||||
|
dst[8] = palette565[src[8]]; |
||||||
|
dst[9] = palette565[src[9]]; |
||||||
|
dst[10] = palette565[src[10]]; |
||||||
|
dst[11] = palette565[src[11]]; |
||||||
|
dst[12] = palette565[src[12]]; |
||||||
|
dst[13] = palette565[src[13]]; |
||||||
|
dst[14] = palette565[src[14]]; |
||||||
|
dst[15] = palette565[src[15]]; |
||||||
|
} |
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Convert 16 pixels using packed 32-bit writes (2 pixels per store) |
||||||
|
* |
||||||
|
* SH4 is efficient at aligned 32-bit loads/stores, so this path halves |
||||||
|
* the number of destination stores compared to scalar 16-bit writes. |
||||||
|
*/ |
||||||
|
inline void Convert16PixelsPacked(const uint8_t *src, uint16_t *dst) |
||||||
|
{ |
||||||
|
#if defined(__SH4__) || defined(__sh__) |
||||||
|
// Pull upcoming source bytes into cache early on SH4.
|
||||||
|
__builtin_prefetch(src + 32, 0, 3); |
||||||
|
__builtin_prefetch(src + 64, 0, 3); |
||||||
|
#endif |
||||||
|
uint32_t *dst32 = reinterpret_cast<uint32_t *>(dst); |
||||||
|
dst32[0] = palette565FirstWord[src[0]] | palette565SecondWord[src[1]]; |
||||||
|
dst32[1] = palette565FirstWord[src[2]] | palette565SecondWord[src[3]]; |
||||||
|
dst32[2] = palette565FirstWord[src[4]] | palette565SecondWord[src[5]]; |
||||||
|
dst32[3] = palette565FirstWord[src[6]] | palette565SecondWord[src[7]]; |
||||||
|
dst32[4] = palette565FirstWord[src[8]] | palette565SecondWord[src[9]]; |
||||||
|
dst32[5] = palette565FirstWord[src[10]] | palette565SecondWord[src[11]]; |
||||||
|
dst32[6] = palette565FirstWord[src[12]] | palette565SecondWord[src[13]]; |
||||||
|
dst32[7] = palette565FirstWord[src[14]] | palette565SecondWord[src[15]]; |
||||||
|
} |
||||||
|
|
||||||
|
void ConvertFrame(const uint8_t *src, uint16_t *dst, int width, int height, int srcPitch, int dstPitch) |
||||||
|
{ |
||||||
|
for (int y = 0; y < height; y++) { |
||||||
|
const uint8_t *srcRow = src + y * srcPitch; |
||||||
|
uint16_t *dstRow = reinterpret_cast<uint16_t *>(reinterpret_cast<uint8_t *>(dst) + y * dstPitch); |
||||||
|
const bool canUsePackedPath = (reinterpret_cast<uintptr_t>(dstRow) & (alignof(uint32_t) - 1)) == 0; |
||||||
|
|
||||||
|
int x = 0; |
||||||
|
if (canUsePackedPath) { |
||||||
|
for (; x + 16 <= width; x += 16) { |
||||||
|
Convert16PixelsPacked(srcRow + x, dstRow + x); |
||||||
|
} |
||||||
|
} else { |
||||||
|
for (; x + 16 <= width; x += 16) { |
||||||
|
Convert16PixelsScalar(srcRow + x, dstRow + x); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
for (; x < width; x++) { |
||||||
|
dstRow[x] = palette565[srcRow[x]]; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} // anonymous namespace
|
||||||
|
|
||||||
|
bool VideoInit([[maybe_unused]] int width, [[maybe_unused]] int height) |
||||||
|
{ |
||||||
|
for (int i = 0; i < 256; i++) { |
||||||
|
UpdatePaletteEntry(i, RGB888toRGB565(i, i, i)); |
||||||
|
} |
||||||
|
|
||||||
|
initialized = true; |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
void VideoShutdown() |
||||||
|
{ |
||||||
|
initialized = false; |
||||||
|
} |
||||||
|
|
||||||
|
void UpdatePalette(const SDL_Palette *palette) |
||||||
|
{ |
||||||
|
if (!palette || !palette->colors) |
||||||
|
return; |
||||||
|
|
||||||
|
UpdatePaletteRange(palette->colors, 0, palette->ncolors); |
||||||
|
} |
||||||
|
|
||||||
|
void UpdatePaletteRange(const SDL_Color *colors, int firstColor, int nColors) |
||||||
|
{ |
||||||
|
if (!colors) |
||||||
|
return; |
||||||
|
|
||||||
|
if (firstColor + nColors > 256) |
||||||
|
nColors = 256 - firstColor; |
||||||
|
|
||||||
|
for (int i = 0; i < nColors; i++) { |
||||||
|
const SDL_Color &c = colors[i]; |
||||||
|
UpdatePaletteEntry(firstColor + i, RGB888toRGB565(c.r, c.g, c.b)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
void ConvertAndUpload(const SDL_Surface *src, SDL_Surface *dst) |
||||||
|
{ |
||||||
|
if (!initialized || !src || !dst) |
||||||
|
return; |
||||||
|
|
||||||
|
const uint8_t *srcPixels = static_cast<const uint8_t *>(src->pixels); |
||||||
|
uint16_t *dstPixels = static_cast<uint16_t *>(dst->pixels); |
||||||
|
|
||||||
|
if (!srcPixels || !dstPixels) |
||||||
|
return; |
||||||
|
|
||||||
|
const int width = src->w < dst->w ? src->w : dst->w; |
||||||
|
const int height = src->h < dst->h ? src->h : dst->h; |
||||||
|
|
||||||
|
ConvertFrame(srcPixels, dstPixels, width, height, src->pitch, dst->pitch); |
||||||
|
} |
||||||
|
|
||||||
|
bool IsInitialized() |
||||||
|
{ |
||||||
|
return initialized; |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace dc
|
||||||
|
} // namespace devilution
|
||||||
|
|
||||||
|
#endif // __DREAMCAST__
|
||||||
@ -0,0 +1,91 @@ |
|||||||
|
/**
|
||||||
|
* @file dc_video.h |
||||||
|
* @brief Dreamcast-specific video conversion layer |
||||||
|
* |
||||||
|
* DevilutionX renders to an 8bpp paletted surface. Dreamcast SDL only |
||||||
|
* supports 16bpp (RGB565). This module provides: |
||||||
|
* |
||||||
|
* 1. Palette management: Converts 8-bit RGB palette entries to RGB565 LUT |
||||||
|
* 2. Frame conversion: Expands 8bpp pixels to 16bpp using the LUT |
||||||
|
* |
||||||
|
* Performance target: <5ms per frame at 640x480 |
||||||
|
*/ |
||||||
|
|
||||||
|
#ifndef DEVILUTIONX_DC_VIDEO_H |
||||||
|
#define DEVILUTIONX_DC_VIDEO_H |
||||||
|
|
||||||
|
#ifdef __DREAMCAST__ |
||||||
|
|
||||||
|
#include <SDL.h> |
||||||
|
#include <cstdint> |
||||||
|
|
||||||
|
namespace devilution { |
||||||
|
namespace dc { |
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize the Dreamcast video conversion layer |
||||||
|
* @param width Screen width (typically 640) |
||||||
|
* @param height Screen height (typically 480) |
||||||
|
* @return true on success |
||||||
|
*/ |
||||||
|
bool VideoInit(int width, int height); |
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Shutdown and free video resources |
||||||
|
*/ |
||||||
|
void VideoShutdown(); |
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update the RGB565 palette lookup table |
||||||
|
* |
||||||
|
* Called when the game changes its palette. Converts each entry |
||||||
|
* from RGB888 to RGB565 format. |
||||||
|
* |
||||||
|
* @param palette SDL palette with 256 RGB entries |
||||||
|
*/ |
||||||
|
void UpdatePalette(const SDL_Palette *palette); |
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Update a range of palette entries |
||||||
|
* @param colors Pointer to SDL_Color array |
||||||
|
* @param firstColor Starting index (0-255) |
||||||
|
* @param nColors Number of colors to update |
||||||
|
*/ |
||||||
|
void UpdatePaletteRange(const SDL_Color *colors, int firstColor, int nColors); |
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Convert an 8bpp frame to 16bpp |
||||||
|
* |
||||||
|
* This is the hot path - called every frame. |
||||||
|
* |
||||||
|
* @param src 8bpp source surface (PalSurface) |
||||||
|
* @param dst 16bpp destination surface (video framebuffer) |
||||||
|
*/ |
||||||
|
void ConvertAndUpload(const SDL_Surface *src, SDL_Surface *dst); |
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Convert RGB888 to RGB565 |
||||||
|
* @param r Red component (0-255) |
||||||
|
* @param g Green component (0-255) |
||||||
|
* @param b Blue component (0-255) |
||||||
|
* @return Packed RGB565 value |
||||||
|
*/ |
||||||
|
inline uint16_t RGB888toRGB565(uint8_t r, uint8_t g, uint8_t b) |
||||||
|
{ |
||||||
|
// RGB565: RRRRRGGGGGGBBBBB
|
||||||
|
return static_cast<uint16_t>( |
||||||
|
((r & 0xF8) << 8) | // 5 bits red
|
||||||
|
((g & 0xFC) << 3) | // 6 bits green
|
||||||
|
((b & 0xF8) >> 3)); // 5 bits blue
|
||||||
|
} |
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if DC video layer is active |
||||||
|
*/ |
||||||
|
bool IsInitialized(); |
||||||
|
|
||||||
|
} // namespace dc
|
||||||
|
} // namespace devilution
|
||||||
|
|
||||||
|
#endif // __DREAMCAST__
|
||||||
|
#endif // DEVILUTIONX_DC_VIDEO_H
|
||||||
@ -0,0 +1,34 @@ |
|||||||
|
/**
|
||||||
|
* @file posix_stubs.c |
||||||
|
* @brief POSIX function stubs for KOS/Dreamcast |
||||||
|
* |
||||||
|
* KOS's newlib is missing some POSIX functions that external libraries expect. |
||||||
|
* This file provides stub implementations. |
||||||
|
*/ |
||||||
|
|
||||||
|
#ifdef __DREAMCAST__ |
||||||
|
|
||||||
|
#include <stdio.h> |
||||||
|
#include <unistd.h> |
||||||
|
|
||||||
|
// POSIX Thread Safety Stubs
|
||||||
|
// KOS doesn't implement these, but libfmt uses them.
|
||||||
|
// Since we are effectively single-threaded for I/O, empty stubs are safe.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Lock a stdio stream (stub - KOS is single-threaded by default) |
||||||
|
*/ |
||||||
|
void flockfile(FILE *file) |
||||||
|
{ |
||||||
|
(void)file; |
||||||
|
} |
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Unlock a stdio stream (stub - KOS is single-threaded by default) |
||||||
|
*/ |
||||||
|
void funlockfile(FILE *file) |
||||||
|
{ |
||||||
|
(void)file; |
||||||
|
} |
||||||
|
|
||||||
|
#endif /* __DREAMCAST__ */ |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
# devilutionX Dreamcast port |
||||||
|
|
||||||
|
## How To Play: |
||||||
|
|
||||||
|
- Build a CDI image using `Packaging/dreamcast/build.sh`. |
||||||
|
- Copy `DIABDAT.MPQ` from your CD or GoG installation to `Packaging/dreamcast/cd_root/` before building (or [extract it from the GoG installer](https://github.com/diasurgical/devilutionX/wiki/Extracting-MPQs-from-the-GoG-installer)). |
||||||
|
- `spawn.mpq` is optional for shareware mode. |
||||||
|
- Use your own data files. Diablo MPQs are proprietary Blizzard assets. |
||||||
|
- Boot the resulting CDI on hardware or in an emulator such as Flycast. |
||||||
|
|
||||||
|
## Building from Source |
||||||
|
|
||||||
|
See [building instructions](../../building.md#dreamcast). |
||||||
|
|
||||||
|
## Controls |
||||||
|
|
||||||
|
- D-pad or analog stick: move hero |
||||||
|
- A: primary action (attack, interact, confirm) |
||||||
|
- B: secondary action, back |
||||||
|
- X: use item |
||||||
|
- Y: cancel, open speedbook |
||||||
|
- Start: menu |
||||||
Loading…
Reference in new issue