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