men100のアイコン画像
men100 2026年01月31日作成 (2026年02月28日更新) © Apache-2.0
製作品 製作品 閲覧数 45
men100 2026年01月31日作成 (2026年02月28日更新) © Apache-2.0 製作品 製作品 閲覧数 45

Spresense で DOOM を動かしてみた

Spresense で DOOM を動かしてみた

概要

Sony のマイコンボード、Spresense で DOOM を動かしてみたという、いわゆる一発ネタです。

DOOM とは?

1993年にアメリカで発売された、パソコン向けのビデオゲームです。プレイヤーは一人の兵士となり、火星の基地や地獄から現れたモンスターたちを銃火器でなぎ倒しながら、迷路のようなステージを攻略していきます。

今日の「Call of Duty」や「Apex Legends」といった、自分の目線で銃を撃つゲーム(FPS)の基礎を築いた伝説的な作品として知られています。

DOOM 移植という文化

DOOMはゲーム自体の面白さもさることながら、そのプログラム設計の美しさで、IT・テック業界では伝説となっています。1997年に開発元がプログラムのエンジン部をオープンソースとして無償公開したことで、世界中のエンジニアがその中身を自由に研究し、改造できるようになりました。非常に効率よく、汎用性が高く設計されているため、本来はゲーム機ではない電卓、プリンター、スマートウォッチ、さらにはATMに至るまで、あらゆる電子機器に移植されています。

「画面が付いているデバイスなら、DOOMが動いて当然」とか一部界隈では言われています。

動機

画面も付けることのできる Spresenseですが、私の調べた範囲でまだ DOOM は動作していないようでした。
それならやってみよかな、というのが動機です。

動画

電源投入からタイトル画面が表示され、デモへと進んでいる様子が確認できます。

ここに動画が表示されます

ゲームパッドを使ってプレイすることもできます。快適とは言えませんが・・・。

ここに動画が表示されます

部品について

部品について

部品名 説明・補足など
Spresense メインボード -
Spresense 拡張ボード -
ディスプレイ 製品名は MSP2807。ドライバに ILI9341 互換のものを使っているなら他でも OK
μSD カード 東芝製の micro SD カード(4GB)を使用。インジケーター用の画像を格納するのに使用しています。他のメーカーのものでも問題ないと思います。容量も4GBも不要で、128MBくらいでも十分です
ゲームパッド I2C 接続のゲームパッドを使用しています。製品名はSTEMMA QT / Qwiic互換 seesaw ミニ I2Cゲームパッドです
モバイルバッテリー パナソニック QE-PL201 を使用しています。低電力モードに対応したものを選びましょう。そうでないと Spresense が低消費電力のため、バッテリーが出力を停止してしまいます。

配線について

Spresense メインボードとモバイルバッテリーとの接続

micro USB オス-USB オスのケーブルで Spresense メインボードとモバイルバッテリーを接続します。
(Spresense 側は micro USB のメスであることに注意)

Spresense 拡張ボードとディスプレイとの接続

Spresense はディスプレイとは SPI 通信によって画面の表示を行います。
ピンの対応としては下記のようになります。

ディスプレイ Spresense 拡張ボード
SDO(MISO) D12
LED 3.3V
SCK D13
SDI(MOSI) D11
DC/RS D9
RESET D8
CS D10
GND GND
VCC 3.3V

Spresense 拡張ボードと I2C ゲームパッドとの接続

Spresense はサーマルカメラセンサーとは I2C 通信によってセンサーの値を取得します。
ピンの対応としては下記のようになります。

I2C ゲームパッド Spresense 拡張ボード
GND GND
SCL D15 (I2C_SCL)
SDA D14 (I2C_SDA)
3V3 3.3V

μSD カードに入れるファイルについて

μSD カードには DOOM の WAD ファイルを配置します。
WAD ファイルにはテクスチャ、サウンド、マップなどのデータが格納されています。
Spresense のメモリの制約から、最もサイズの小さい doom1.wad の配置を強くおすすめします。

おそらく他の タイトルのWAD ファイルを読み込むとメモリの確保に失敗し、止まってしまいます。

シェアウェアとなっており、誰でも利用できます。例えば下記からダウンロード可能です。
DOOM1.WAD - The Doom Wiki at DoomWiki.org

repo 構成

SenseDoom

men100/SenseDoom: Doom for Spresense

こちらが Project の Repository になっています。
Spresense に関わるコードが入っており、PlatformIO からビルド・書き込みを行います。

doomgeneric

men100/doomgeneric: Easily portable doom

DOOM はオープンソースで公開されたため、様々な実装が存在します。
その中でも特に移植性を意識して開発された doomgeneric というものがあり、こちらを本 Project でも利用しています。
なるべく Spresense 依存のコードは書かないようにしたかったのですが、結局結構書いてしまいました。

SenseDoom からは submodule として組み込まれ、参照しています。

ソースコード

前で述べた各 Repo の全ソースコードをこちらに記載するとあまりに長大になってしまうので、
今回は工夫したポイントを示しつつ、それに関するソースコードを示すことにしたいと思います。

メモリ確保

最後まで苦労したのがメモリ確保ですね。

ゲームエンジンである doomgeneric では起動時にメモリを数MB確保して、それをメモリプールとしてエンジン内部で確保と解放を繰り返します。その初期設定が6MB。Spresense の 1.5MB では絶対に確保できません。

ただ、一番軽量の doom1.wad はファイルサイズが 4MB のため、ゲーム進行に合わせて確保するような動作なら耐えられるのでは?と想定。PC 上でビルド&実行して実験してみた結果、想定通り起動直後にガッツリ確保するような動作ではありませんでした。また、メモリの確保量も1MB程度確保できれば doom1.wad ならある程度動作するようでした(長時間動かしているとメモリ確保に失敗してしまうこともありました)

そうしてエンジン用に1MBのメモリを確保することが目標になったのですが、これも大変。
エンジン自体が300KB程度メモリを持っていってしまうし、液晶に書き込むための Screen Buffer が 150KB あり、これだけで450KB。エンジン用に1MB確保したら他のコードは50KBしか使えず、まず無理。

そこで Spresense のチュートリアルを眺めていたら、素晴らしい機能を見つけました。

GNSS RAMメモリ使用機能について

GNSS を使用しない場合、GNSS の firmware の書き込みエリアを汎用 RAM として使えるという機能です。
これを使って、640KB のメモリを確保することができましたが、まだ足りません。タイトル画面は出るのですが、そこで止まってしまいます。

考えた末に、メモリプールを二段構成にすることにしました。
つまり、Spresense の RAM (メインメモリと呼称) を Primary RAM とし、ここが枯渇したら GNSS RAM を Secondary RAM として確保に行くようにエンジン側のアロケータを修正しました。

結果としてこのアプローチはうまくいき、ゲームが完全に進行するようになりました。現状はメインメモリを 572KB、GNSS RAM を640KB の1.2MB程度の確保量になっています。ゲームの動きとして細かいメモリ確保を繰り返すようなのも良かったです。1つのメモリプールを超えるような大きいサイズを持っていくようなことがあると、二段構成はワークしなくなってしまいますので。

ただ、このアプローチには痛い副作用がありました。それはチュートリアルにもある、

アプリケーションが通常利用している RAM に比べて 1/8 以下にパフォーマンスが落ちます。

というものです。動作させて画面を見ていると、GNSS RAM へのアクセスがあるとガクッとパフォーマンスが落ちるのが分かります。まあ贅沢を言っているとは思いますが・・・。

i_system.c

I_GetSecondaryZone 関数で GNSS RAM、AutoAllocMemory 関数でメインメモリを割り当てています。

doomgeneric/i_system.c

// // Copyright(C) 1993-1996 Id Software, Inc. // Copyright(C) 2005-2014 Simon Howard // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // DESCRIPTION: // #include <stdlib.h> #include <stdio.h> #include <string.h> #include <stdarg.h> #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN #include <windows.h> #else #include <unistd.h> #endif #ifdef ORIGCODE #include "SDL.h" #endif #include "config.h" #include "deh_str.h" #include "doomtype.h" #include "m_argv.h" #include "m_config.h" #include "m_misc.h" #include "i_joystick.h" #include "i_sound.h" #include "i_timer.h" #include "i_video.h" #include "i_system.h" #include "w_wad.h" #include "z_zone.h" #ifdef __MACOSX__ #include <CoreFoundation/CFUserNotification.h> #endif #define DEFAULT_RAM 6 /* MiB */ #define MIN_RAM 6 /* MiB */ typedef struct atexit_listentry_s atexit_listentry_t; struct atexit_listentry_s { atexit_func_t func; boolean run_on_error; atexit_listentry_t *next; }; static atexit_listentry_t *exit_funcs = NULL; void I_AtExit(atexit_func_t func, boolean run_on_error) { atexit_listentry_t *entry; entry = malloc(sizeof(*entry)); entry->func = func; entry->run_on_error = run_on_error; entry->next = exit_funcs; exit_funcs = entry; } // Tactile feedback function, probably used for the Logitech Cyberman void I_Tactile(int on, int off, int total) { } // Zone memory auto-allocation function that allocates the zone size // by trying progressively smaller zone sizes until one is found that // works. #include <arch/chip/gnssram.h> void I_GetSecondaryZone(byte **ptr, int *size) { // GNSS RAM は 640KB あるが、管理領域があるからか、このサイズ以上は確保できない *size = 654988; *ptr = up_gnssram_malloc(*size); if (ptr == NULL) { I_Error("I_GetSecondaryZone: Unable to allocate %i Bytes of RAM for zone", *size); } } // Primary Zone in Main RAM (576KiB) #define PRIMARY_RAM_SIZE (576 * 1024) static byte doom_primary_zone[PRIMARY_RAM_SIZE]; static byte *AutoAllocMemory(int *size, int default_ram, int min_ram) { byte *zonemem; // Allocate the zone memory. This loop tries progressively smaller // zone sizes until a size is found that can be allocated. // If we used the -mb command line parameter, only the parameter // provided is accepted. *size = PRIMARY_RAM_SIZE; // zonemem は Main RAM に格納 zonemem = doom_primary_zone; printf("AutoAllocMemory: zonemem=%p, size=%i\n", zonemem, *size); if (zonemem == NULL) { I_Error("AutoAllocMemory: Unable to allocate %i Bytes of RAM for zone", *size); } return zonemem; } byte *I_ZoneBase (int *size) { byte *zonemem; int min_ram, default_ram; int p; //! // @arg <mb> // // Specify the heap size, in MiB (default 16). // p = M_CheckParmWithArgs("-mb", 1); if (p > 0) { default_ram = atoi(myargv[p+1]); min_ram = default_ram; } else { default_ram = DEFAULT_RAM; min_ram = MIN_RAM; } zonemem = AutoAllocMemory(size, default_ram, min_ram); printf("zone memory: %p, %x allocated for zone\n", zonemem, *size); return zonemem; } void I_PrintBanner(char *msg) { int i; int spaces = 35 - (strlen(msg) / 2); for (i=0; i<spaces; ++i) putchar(' '); puts(msg); } void I_PrintDivider(void) { int i; for (i=0; i<75; ++i) { putchar('='); } putchar('\n'); } void I_PrintStartupBanner(char *gamedescription) { I_PrintDivider(); I_PrintBanner(gamedescription); I_PrintDivider(); printf( " " PACKAGE_NAME " is free software, covered by the GNU General Public\n" " License. There is NO warranty; not even for MERCHANTABILITY or FITNESS\n" " FOR A PARTICULAR PURPOSE. You are welcome to change and distribute\n" " copies under certain conditions. See the source for more information.\n"); I_PrintDivider(); } // // I_ConsoleStdout // // Returns true if stdout is a real console, false if it is a file // boolean I_ConsoleStdout(void) { #ifdef _WIN32 // SDL "helpfully" always redirects stdout to a file. return 0; #else #if ORIGCODE return isatty(fileno(stdout)); #else return 0; #endif #endif } // // I_Init // /* void I_Init (void) { I_CheckIsScreensaver(); I_InitTimer(); I_InitJoystick(); } void I_BindVariables(void) { I_BindVideoVariables(); I_BindJoystickVariables(); I_BindSoundVariables(); } */ // // I_Quit // void I_Quit (void) { atexit_listentry_t *entry; // Run through all exit functions entry = exit_funcs; while (entry != NULL) { entry->func(); entry = entry->next; } #if ORIGCODE SDL_Quit(); exit(0); #endif } #if !defined(_WIN32) && !defined(__MACOSX__) && !defined(__DJGPP__) #define ZENITY_BINARY "/usr/bin/zenity" // returns non-zero if zenity is available static int ZenityAvailable(void) { return system(ZENITY_BINARY " --help >/dev/null 2>&1") == 0; } // Escape special characters in the given string so that they can be // safely enclosed in shell quotes. static char *EscapeShellString(char *string) { char *result; char *r, *s; // In the worst case, every character might be escaped. result = malloc(strlen(string) * 2 + 3); r = result; // Enclosing quotes. *r = '"'; ++r; for (s = string; *s != '\0'; ++s) { // From the bash manual: // // "Enclosing characters in double quotes preserves the literal // value of all characters within the quotes, with the exception // of $, `, \, and, when history expansion is enabled, !." // // Therefore, escape these characters by prefixing with a backslash. if (strchr("$`\\!", *s) != NULL) { *r = '\\'; ++r; } *r = *s; ++r; } // Enclosing quotes. *r = '"'; ++r; *r = '\0'; return result; } // Open a native error box with a message using zenity static int ZenityErrorBox(char *message) { int result; char *escaped_message; char *errorboxpath; static size_t errorboxpath_size; if (!ZenityAvailable()) { return 0; } escaped_message = EscapeShellString(message); errorboxpath_size = strlen(ZENITY_BINARY) + strlen(escaped_message) + 19; errorboxpath = malloc(errorboxpath_size); M_snprintf(errorboxpath, errorboxpath_size, "%s --error --text=%s", ZENITY_BINARY, escaped_message); result = system(errorboxpath); free(errorboxpath); free(escaped_message); return result; } #endif /* !defined(_WIN32) && !defined(__MACOSX__) && !defined(__DJGPP__) */ // // I_Error // static boolean already_quitting = false; void I_Error (char *error, ...) { char msgbuf[512]; va_list argptr; atexit_listentry_t *entry; boolean exit_gui_popup; if (already_quitting) { fprintf(stderr, "Warning: recursive call to I_Error detected.\n"); #if ORIGCODE exit(-1); #endif } else { already_quitting = true; } // Message first. va_start(argptr, error); //fprintf(stderr, "\nError: "); vfprintf(stderr, error, argptr); fprintf(stderr, "\n\n"); va_end(argptr); fflush(stderr); // Write a copy of the message into buffer. va_start(argptr, error); memset(msgbuf, 0, sizeof(msgbuf)); M_vsnprintf(msgbuf, sizeof(msgbuf), error, argptr); va_end(argptr); // Shutdown. Here might be other errors. entry = exit_funcs; while (entry != NULL) { if (entry->run_on_error) { entry->func(); } entry = entry->next; } exit_gui_popup = !M_ParmExists("-nogui"); // Pop up a GUI dialog box to show the error message, if the // game was not run from the console (and the user will // therefore be unable to otherwise see the message). if (exit_gui_popup && !I_ConsoleStdout()) #ifdef _WIN32 { wchar_t wmsgbuf[512]; MultiByteToWideChar(CP_ACP, 0, msgbuf, strlen(msgbuf) + 1, wmsgbuf, sizeof(wmsgbuf)); MessageBoxW(NULL, wmsgbuf, L"", MB_OK); } #elif defined(__MACOSX__) { CFStringRef message; int i; // The CoreFoundation message box wraps text lines, so replace // newline characters with spaces so that multiline messages // are continuous. for (i = 0; msgbuf[i] != '\0'; ++i) { if (msgbuf[i] == '\n') { msgbuf[i] = ' '; } } message = CFStringCreateWithCString(NULL, msgbuf, kCFStringEncodingUTF8); CFUserNotificationDisplayNotice(0, kCFUserNotificationCautionAlertLevel, NULL, NULL, NULL, CFSTR(PACKAGE_STRING), message, NULL); } #elif defined(__DJGPP__) { printf("%s\n", msgbuf); exit(-1); } #else { ZenityErrorBox(msgbuf); } #endif // abort(); #if ORIGCODE SDL_Quit(); exit(-1); #else exit(-1); #endif } // // Read Access Violation emulation. // // From PrBoom+, by entryway. // // C:\>debug // -d 0:0 // // DOS 6.22: // 0000:0000 (57 92 19 00) F4 06 70 00-(16 00) // DOS 7.1: // 0000:0000 (9E 0F C9 00) 65 04 70 00-(16 00) // Win98: // 0000:0000 (9E 0F C9 00) 65 04 70 00-(16 00) // DOSBox under XP: // 0000:0000 (00 00 00 F1) ?? ?? ?? 00-(07 00) #define DOS_MEM_DUMP_SIZE 10 static const unsigned char mem_dump_dos622[DOS_MEM_DUMP_SIZE] = { 0x57, 0x92, 0x19, 0x00, 0xF4, 0x06, 0x70, 0x00, 0x16, 0x00}; static const unsigned char mem_dump_win98[DOS_MEM_DUMP_SIZE] = { 0x9E, 0x0F, 0xC9, 0x00, 0x65, 0x04, 0x70, 0x00, 0x16, 0x00}; static const unsigned char mem_dump_dosbox[DOS_MEM_DUMP_SIZE] = { 0x00, 0x00, 0x00, 0xF1, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00}; static unsigned char mem_dump_custom[DOS_MEM_DUMP_SIZE]; static const unsigned char *dos_mem_dump = mem_dump_dos622; boolean I_GetMemoryValue(unsigned int offset, void *value, int size) { static boolean firsttime = true; if (firsttime) { int p, i, val; firsttime = false; i = 0; //! // @category compat // @arg <version> // // Specify DOS version to emulate for NULL pointer dereference // emulation. Supported versions are: dos622, dos71, dosbox. // The default is to emulate DOS 7.1 (Windows 98). // p = M_CheckParmWithArgs("-setmem", 1); if (p > 0) { if (!strcasecmp(myargv[p + 1], "dos622")) { dos_mem_dump = mem_dump_dos622; } if (!strcasecmp(myargv[p + 1], "dos71")) { dos_mem_dump = mem_dump_win98; } else if (!strcasecmp(myargv[p + 1], "dosbox")) { dos_mem_dump = mem_dump_dosbox; } else { for (i = 0; i < DOS_MEM_DUMP_SIZE; ++i) { ++p; if (p >= myargc || myargv[p][0] == '-') { break; } M_StrToInt(myargv[p], &val); mem_dump_custom[i++] = (unsigned char) val; } dos_mem_dump = mem_dump_custom; } } } switch (size) { case 1: *((unsigned char *) value) = dos_mem_dump[offset]; return true; case 2: *((unsigned short *) value) = dos_mem_dump[offset] | (dos_mem_dump[offset + 1] << 8); return true; case 4: *((unsigned int *) value) = dos_mem_dump[offset] | (dos_mem_dump[offset + 1] << 8) | (dos_mem_dump[offset + 2] << 16) | (dos_mem_dump[offset + 3] << 24); return true; } return false; }

z_zone.c

Z_Malloc_Zone 関数を新設し、メモリプールを指定してメモリ確保をするようにしています。
Z_Free 関数を修正し、どちらのメモリプールかを判定して適切に解放するようにしました。

doomgeneric/z_zone.c

// // Copyright(C) 1993-1996 Id Software, Inc. // Copyright(C) 2005-2014 Simon Howard // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // DESCRIPTION: // Zone Memory Allocation. Neat. // #include "z_zone.h" #include "i_system.h" #include "doomtype.h" // // ZONE MEMORY ALLOCATION // // There is never any space between memblocks, // and there will never be two contiguous free memblocks. // The rover can be left pointing at a non-empty block. // // It is of no value to free a cachable block, // because it will get overwritten automatically if needed. // #define MEM_ALIGN sizeof(void *) #define ZONEID 0x1d4a11 typedef struct memblock_s { int size; // including the header and possibly tiny fragments void** user; int tag; // PU_FREE if this is free int id; // should be ZONEID struct memblock_s* next; struct memblock_s* prev; } memblock_t; typedef struct { // total bytes malloced, including header int size; // start / end cap for linked list memblock_t blocklist; memblock_t* rover; } memzone_t; memzone_t* mainzone; memzone_t* secondaryzone; extern void I_GetSecondaryZone(byte **ptr, int *size); // // Z_ClearZone // void Z_ClearZone (memzone_t* zone) { memblock_t* block; // set the entire zone to one free block zone->blocklist.next = zone->blocklist.prev = block = (memblock_t *)( (byte *)zone + sizeof(memzone_t) ); zone->blocklist.user = (void *)zone; zone->blocklist.tag = PU_STATIC; zone->rover = block; block->prev = block->next = &zone->blocklist; // a free block. block->tag = PU_FREE; block->size = zone->size - sizeof(memzone_t); } // // Z_Init // void Z_Init (void) { memblock_t* block; int size; // Initialize Main Zone (Main RAM) mainzone = (memzone_t *)I_ZoneBase (&size); mainzone->size = size; Z_ClearZone(mainzone); // Initialize Secondary Zone (GNSS RAM) byte* sec_ptr; int sec_size; I_GetSecondaryZone(&sec_ptr, &sec_size); secondaryzone = (memzone_t *)sec_ptr; secondaryzone->size = sec_size; Z_ClearZone(secondaryzone); printf("Z_Init: Main Zone (GNSS) %p size %d, Secondary Zone (RAM) %p size %d\n", mainzone, mainzone->size, secondaryzone, secondaryzone->size); } // // Z_Free // void Z_Free (void* ptr) { memblock_t* block; memblock_t* other; memzone_t* zone; block = (memblock_t *) ( (byte *)ptr - sizeof(memblock_t)); if (block->id != ZONEID) I_Error ("Z_Free: freed a pointer without ZONEID"); // ブロックがどちらの zone に属するかを判定 if ((byte*)block >= (byte*)mainzone && (byte*)block < (byte*)mainzone + mainzone->size) { zone = mainzone; } else if ((byte*)block >= (byte*)secondaryzone && (byte*)block < (byte*)secondaryzone + secondaryzone->size) { zone = secondaryzone; } else { I_Error("Z_Free: Pointer %p is not in any zone!", ptr); return; } if (block->tag != PU_FREE && block->user != NULL) { // clear the user's mark *block->user = 0; } // mark as free block->tag = PU_FREE; block->user = NULL; block->id = 0; other = block->prev; if (other->tag == PU_FREE) { // merge with previous free block other->size += block->size; other->next = block->next; other->next->prev = other; if (block == zone->rover) zone->rover = other; block = other; } other = block->next; if (other->tag == PU_FREE) { // merge the next free block onto the end block->size += other->size; block->next = other->next; block->next->prev = block; if (other == zone->rover) zone->rover = block; } } // // Z_Malloc // You can pass a NULL user if the tag is < PU_PURGELEVEL. // #define MINFRAGMENT 64 // 特定の zone から allocate を試行する helper 関数 void* Z_Malloc_Zone(memzone_t* zone, int size, int tag, void* user) { int extra; memblock_t* start; memblock_t* rover; memblock_t* newblock; memblock_t* base; // 呼び手 (Z_Malloc) で実施済 // size = (size + MEM_ALIGN - 1) & ~(MEM_ALIGN - 1); // size += sizeof(memblock_t); base = zone->rover; if (base->prev->tag == PU_FREE) base = base->prev; rover = base; start = base->prev; do { if (rover == start) { // リストを最後までスキャンした return NULL; } if (rover->tag != PU_FREE) { if (rover->tag < PU_PURGELEVEL) { base = rover = rover->next; } else { base = base->prev; Z_Free ((byte *)rover+sizeof(memblock_t)); base = base->next; rover = base->next; } } else { rover = rover->next; } } while (base->tag != PU_FREE || base->size < size); extra = base->size - size; if (extra > MINFRAGMENT) { newblock = (memblock_t *) ((byte *)base + size ); newblock->size = extra; newblock->tag = PU_FREE; newblock->user = NULL; newblock->prev = base; newblock->next = base->next; newblock->next->prev = newblock; base->next = newblock; base->size = size; } if (user) { base->user = user; *(void **)user = (void *) ((byte *)base + sizeof(memblock_t)); } else { if (tag >= PU_PURGELEVEL) I_Error ("Z_Malloc: an owner is required for purgable blocks"); base->user = NULL; } base->tag = tag; base->id = ZONEID; zone->rover = base->next; return (void *) ((byte *)base + sizeof(memblock_t)); } void* Z_Malloc ( int size, int tag, void* user ) { void* result; int original_size = size; size = (size + MEM_ALIGN - 1) & ~(MEM_ALIGN - 1); size += sizeof(memblock_t); // Try Main Zone (Main RAM) result = Z_Malloc_Zone(mainzone, size, tag, user); if (result == NULL) { // Try Secondary Zone (GNSS RAM) // printf("Z_Malloc: Main zone full, trying secondary for %d bytes\n", original_size); result = Z_Malloc_Zone(secondaryzone, size, tag, user); } if (result == NULL) { if (tag >= PU_CACHE) { // キャッシュ可能なブロックなので、NULL を返しても救える可能性がある printf("Z_Malloc: failed on allocation of %i bytes (Both zones full)\n", original_size); return NULL; } // こちらは救えない I_Error ("Z_Malloc: failed on allocation of %i bytes", original_size); } return result; } // // Z_FreeTags // void Z_FreeTags ( int lowtag, int hightag ) { memblock_t* block; memblock_t* next; for (block = mainzone->blocklist.next ; block != &mainzone->blocklist ; block = next) { // get link before freeing next = block->next; // free block? if (block->tag == PU_FREE) continue; if (block->tag >= lowtag && block->tag <= hightag) Z_Free ( (byte *)block+sizeof(memblock_t)); } } // // Z_DumpHeap // Note: TFileDumpHeap( stdout ) ? // void Z_DumpHeap ( int lowtag, int hightag ) { memblock_t* block; printf ("zone size: %i location: %p\n", mainzone->size,mainzone); printf ("tag range: %i to %i\n", lowtag, hightag); for (block = mainzone->blocklist.next ; ; block = block->next) { if (block->tag >= lowtag && block->tag <= hightag) printf ("block:%p size:%7i user:%p tag:%3i\n", block, block->size, block->user, block->tag); if (block->next == &mainzone->blocklist) { // all blocks have been hit break; } if ( (byte *)block + block->size != (byte *)block->next) printf ("ERROR: block size does not touch the next block\n"); if ( block->next->prev != block) printf ("ERROR: next block doesn't have proper back link\n"); if (block->tag == PU_FREE && block->next->tag == PU_FREE) printf ("ERROR: two consecutive free blocks\n"); } } // // Z_FileDumpHeap // void Z_FileDumpHeap (FILE* f) { memblock_t* block; fprintf (f,"zone size: %i location: %p\n",mainzone->size,mainzone); for (block = mainzone->blocklist.next ; ; block = block->next) { fprintf (f,"block:%p size:%7i user:%p tag:%3i\n", block, block->size, block->user, block->tag); if (block->next == &mainzone->blocklist) { // all blocks have been hit break; } if ( (byte *)block + block->size != (byte *)block->next) fprintf (f,"ERROR: block size does not touch the next block\n"); if ( block->next->prev != block) fprintf (f,"ERROR: next block doesn't have proper back link\n"); if (block->tag == PU_FREE && block->next->tag == PU_FREE) fprintf (f,"ERROR: two consecutive free blocks\n"); } } // // Z_CheckHeap // void Z_CheckHeap (void) { memblock_t* block; for (block = mainzone->blocklist.next ; ; block = block->next) { if (block->next == &mainzone->blocklist) { // all blocks have been hit break; } if ( (byte *)block + block->size != (byte *)block->next) I_Error ("Z_CheckHeap: block size does not touch the next block\n"); if ( block->next->prev != block) I_Error ("Z_CheckHeap: next block doesn't have proper back link\n"); if (block->tag == PU_FREE && block->next->tag == PU_FREE) I_Error ("Z_CheckHeap: two consecutive free blocks\n"); } } // // Z_ChangeTag // void Z_ChangeTag2(void *ptr, int tag, char *file, int line) { memblock_t* block; block = (memblock_t *) ((byte *)ptr - sizeof(memblock_t)); if (block->id != ZONEID) I_Error("%s:%i: Z_ChangeTag: block without a ZONEID!", file, line); if (tag >= PU_PURGELEVEL && block->user == NULL) I_Error("%s:%i: Z_ChangeTag: an owner is required " "for purgable blocks", file, line); block->tag = tag; } void Z_ChangeUser(void *ptr, void **user) { memblock_t* block; block = (memblock_t *) ((byte *)ptr - sizeof(memblock_t)); if (block->id != ZONEID) { I_Error("Z_ChangeUser: Tried to change user for invalid block!"); } block->user = user; *user = ptr; } // // Z_FreeMemory // int Z_FreeMemory (void) { memblock_t* block; int free; free = 0; for (block = mainzone->blocklist.next ; block != &mainzone->blocklist; block = block->next) { if (block->tag == PU_FREE || block->tag >= PU_PURGELEVEL) free += block->size; } return free; } unsigned int Z_ZoneSize(void) { return mainzone->size; }

描画関連

LCD(ILI9341) への描画は LovyanGFX を使って行っています。
doomgeneric は ILI9341 でサポートしている RGB565 の出力にはオプション上は対応しているのですが、typedef の対応が抜けて 32bit (RGBA8888) で出力してしまう不具合がありました。そのため USE_RGB565 という定義を追加して対応しました。

また、doomgeneric での出力される RGB565 は Litte Endian、ILI9341 への入力する RGB565 は Bit Endian という Endian の違いがあったので、当初は化けて表示されていました。そこはさすがの LovyanGFX、そのケアをする setSwapBytes 関数があるので解決しました。

doomgeneric.h

USE_RGB565 周りが追記箇所です。

doomgeneric/doomgeneric.h

#ifndef DOOM_GENERIC #define DOOM_GENERIC #include <stdlib.h> #include <stdint.h> #ifndef DOOMGENERIC_RESX #define DOOMGENERIC_RESX 640 #endif // DOOMGENERIC_RESX #ifndef DOOMGENERIC_RESY #define DOOMGENERIC_RESY 400 #endif // DOOMGENERIC_RESY #ifdef USE_RGB565 typedef uint16_t pixel_t; #elif defined(CMAP256) typedef uint8_t pixel_t; #else // CMAP256 typedef uint32_t pixel_t; #endif // USE_RGB565 extern pixel_t* DG_ScreenBuffer; #ifdef __cplusplus extern "C" { #endif void doomgeneric_Create(int argc, const char **argv); void doomgeneric_Tick(); //Implement below functions for your platform void DG_Init(); void DG_DrawFrame(); void DG_SleepMs(uint32_t ms); uint32_t DG_GetTicksMs(); int DG_GetKey(int* pressed, unsigned char* key); void DG_SetWindowTitle(const char * title); #ifdef __cplusplus } #endif #endif //DOOM_GENERIC

doomgeneric_spresense.cpp

DG_Init 関数での gfx.setSwapBytes 関数呼び出しで色化けをしないようにしています。

SenseDoom/doomgeneric_spresense.cpp

#include <arch/chip/gnssram.h> #include <Arduino.h> #include <Wire.h> #include <Adafruit_seesaw.h> #include <doomtype.h> #include <doomgeneric.h> #include "spresense_debug.h" #include "spresense_sd.h" #include "LGFX_SPRESENSE.hpp" #include "doomkeys.h" // USE_RGB565 のとき、pixel_t は uint16_t pixel_t* DG_ScreenBuffer = nullptr; // Screen Buffer (320 x 240 x 16bit = 125KiB) #define bufferArrayNum (DOOMGENERIC_RESX * DOOMGENERIC_RESY) static pixel_t screen_buffer_array[bufferArrayNum]; LGFX gfx; Adafruit_seesaw gamepad; bool gamepad_found = false; // Button mapping for Adafruit Gamepad QT #define BUTTON_A 5 #define BUTTON_B 1 #define BUTTON_X 6 #define BUTTON_Y 2 #define BUTTON_START 16 #define BUTTON_SELECT 0 // Joystick Logic #define JOY_ADDR 0x50 #define JOYSTICK_X_PIN 14 #define JOYSTICK_Y_PIN 15 #define JOY_DEADZONE 200 #define JOY_CENTER 512 uint32_t last_buttons = 0; int last_joy_x = 0; // -1, 0, 1 int last_joy_y = 0; // -1, 0, 1 extern "C" { void DG_Init() { spresense_debug_init(); spresense_sd_init(); up_gnssram_initialize(); gfx.init(); gfx.setBrightness(128); // バックライトの明るさ (0-255) gfx.setRotation(1); // ディスプレイの向き (0=縦, 1=横, 2=逆縦, 3=逆横) // 出力バッファは Litte Endian な一方、ILI9341 への入力は Big Endian である必要がある // ここで Byte Swap して、色化けしないようにする gfx.setSwapBytes(true); DG_ScreenBuffer = screen_buffer_array; if (DG_ScreenBuffer == nullptr) { spresense_printf("DG_Init: Failed to allocate screen buffer!\n"); while(1); } // Init Gamepad Wire.begin(); if (!gamepad.begin(JOY_ADDR)) { spresense_printf("Seesaw gamepad not found! Check wiring.\n"); gamepad_found = false; } else { gamepad_found = true; } if (gamepad_found) { spresense_printf("Seesaw gamepad started at 0x%02X!\n", JOY_ADDR); uint32_t mask = (1UL << BUTTON_A) | (1UL << BUTTON_B) | (1UL << BUTTON_X) | (1UL << BUTTON_Y) | (1UL << BUTTON_START) | (1UL << BUTTON_SELECT); gamepad.pinModeBulk(mask, INPUT_PULLUP); } } void DG_DrawFrame() { // USE_RGB565 により RGB565 バッファが来るので、直接入力できる gfx.pushImage(0, 40, DOOMGENERIC_RESX, DOOMGENERIC_RESY, (uint16_t*)DG_ScreenBuffer); static uint32_t lastMillis = 0; static int frameCount = 0; static float fps = 0.0f; frameCount++; uint32_t currentMillis = millis(); if (currentMillis - lastMillis >= 1000) { fps = frameCount * 1000.0f / (currentMillis - lastMillis); frameCount = 0; lastMillis = currentMillis; // FPS 描画 gfx.fillRect(0, 0, 320, 40, TFT_BLACK); gfx.setTextColor(TFT_WHITE, TFT_BLACK); gfx.setFont(&fonts::Font2); // Use a standard font gfx.setCursor(0, 0); gfx.printf("FPS: %.1f", fps); } } void DG_SleepMs(uint32_t ms) { delay(ms); } uint32_t DG_GetTicksMs() { return millis(); } int DG_GetKey(int* pressed, unsigned char* key) { if (!gamepad_found) { return 0; } // 理想的には Queue などで複数入力を溜め込むのが望ましいが、 // シンプルに1回の読み出しで1つの変更のみを処理する // Read Buttons uint32_t buttons = gamepad.digitalReadBulk( (1UL << BUTTON_A) | (1UL << BUTTON_B) | (1UL << BUTTON_X) | (1UL << BUTTON_Y) | (1UL << BUTTON_START) | (1UL << BUTTON_SELECT)); // Invert logic (INPUT_PULLUP: 0 is pressed) buttons = ~buttons; // Check changes uint32_t changed = buttons ^ last_buttons; if (changed) { // Find the first changed bit for (int i = 0; i < 32; i++) { if (changed & (1UL << i)) { *pressed = (buttons & (1UL << i)) ? 1 : 0; // Map to Doom Keys switch(i) { case BUTTON_A: *key = KEY_USE; break; // A -> Use case BUTTON_B: *key = KEY_FIRE; break; // B -> Fire case BUTTON_X: *key = KEY_STRAFE_R; break; // X -> Strafe Right case BUTTON_Y: *key = KEY_STRAFE_L; break; // Y -> Strafe Left case BUTTON_START: *key = KEY_ENTER; break; // START -> Enter case BUTTON_SELECT: *key = KEY_TAB; break; // SELECT -> Tab (Map) default: *key = 0; break; } if (*key != 0) { // 他の bit は次回処理 last_buttons ^= (1UL << i); return 1; // Event generated } } } // 変更があったが 0 に map された last_buttons = buttons; } // アナログパッドの読み込み、20ms に一回 static uint32_t last_joy_read = 0; if (millis() - last_joy_read > 20) { last_joy_read = millis(); int x_val = gamepad.analogRead(JOYSTICK_X_PIN); int y_val = gamepad.analogRead(JOYSTICK_Y_PIN); int new_joy_x = 0; if (x_val < JOY_CENTER - JOY_DEADZONE) new_joy_x = -1; // Left else if (x_val > JOY_CENTER + JOY_DEADZONE) new_joy_x = 1; // Right int new_joy_y = 0; if (y_val < JOY_CENTER - JOY_DEADZONE) new_joy_y = 1; // Up else if (y_val > JOY_CENTER + JOY_DEADZONE) new_joy_y = -1; // Down // Process X change if (new_joy_x != last_joy_x) { if (last_joy_x != 0) { // Release old direction *pressed = 0; *key = (last_joy_x == -1) ? KEY_RIGHTARROW : KEY_LEFTARROW; // ロジック反転 (うまく動くので) last_joy_x = 0; // Intermediate state return 1; } if (new_joy_x != 0) { // Press new direction *pressed = 1; *key = (new_joy_x == -1) ? KEY_RIGHTARROW : KEY_LEFTARROW; // ロジック反転 (うまく動くので) last_joy_x = new_joy_x; return 1; } } // Process Y change if (new_joy_y != last_joy_y) { if (last_joy_y != 0) { // Release old *pressed = 0; *key = (last_joy_y == 1) ? KEY_UPARROW : KEY_DOWNARROW; last_joy_y = 0; return 1; } if (new_joy_y != 0) { // Press new *pressed = 1; *key = (new_joy_y == 1) ? KEY_UPARROW : KEY_DOWNARROW; last_joy_y = new_joy_y; return 1; } } } return 0; } void DG_SetWindowTitle(const char * title) { (void)title; // do nothing } void I_Endoom(byte *endoom_data) { (void)endoom_data; // do nothing } } // extern "C"

hook 関連

doomgeneric は元々 PC 用ゲームのコードを base にしているため、FILE 操作を行ったりデバッグプリントに printf を使っています。これを Spresense でシームレスに動作させるため、実装を差し替えました。

各種 FILE 操作 (Open, Close, Read) については SD カードの操作に差し替えるべく、Spresense 用の実装を用意してそちらをビルド対象にするようにしました。

printf についてはシリアルログ版の printf を用意し、build_flags で強制的に差し替えました。

doom_file_io.cpp

stdc_wad_file がエンジンにおいて wad ファイルを操作する関数を示す構造体で、そちらに自前で実装した関数を指定しています。

SenseDoom/doom_file_io.cpp

#include <Arduino.h> #include <SDHCI.h> extern "C" { #include "w_file.h" #include "z_zone.h" #include "spresense_debug.h" } extern "C" { // The wad_file_t struct holds information about an open WAD file typedef struct { wad_file_t wad; File file; } spresense_wad_file_t; // The wad_file_class_t struct holds function pointers for file operations extern wad_file_class_t stdc_wad_file; static wad_file_t* W_Spresense_OpenFile(char* path) { spresense_wad_file_t* result; File f; // Use the global sdCard object from spresense_sd.cpp extern SDClass sdCard; f = sdCard.open(path, FILE_READ); if (!f) { spresense_printf("Failed to open WAD file: %s\n", path); return NULL; } spresense_printf("Successfully opened WAD file: %s\n", path); result = (spresense_wad_file_t*)Z_Malloc(sizeof(spresense_wad_file_t), PU_STATIC, 0); result->wad.file_class = &stdc_wad_file; result->wad.mapped = NULL; result->wad.length = f.size(); result->file = f; return &result->wad; } static void W_Spresense_CloseFile(wad_file_t* wad) { spresense_wad_file_t* spresense_wad = (spresense_wad_file_t*)wad; spresense_wad->file.close(); Z_Free(spresense_wad); } static size_t W_Spresense_Read(wad_file_t* wad, unsigned int offset, void* buffer, size_t buffer_len) { spresense_wad_file_t* spresense_wad = (spresense_wad_file_t*)wad; if (!spresense_wad->file.seek(offset)) { return 0; } return spresense_wad->file.read(buffer, buffer_len); } wad_file_class_t stdc_wad_file = { W_Spresense_OpenFile, W_Spresense_CloseFile, W_Spresense_Read, }; } // extern "C"

platformio.ini

build_flags の include オプションですべてのファイルに強制的に指定のヘッダファイルを読み込ませることができます。

SenseDoom/platformio.ini

; PlatformIO Project Configuration File ; ; Build options: build flags, source filter ; Upload options: custom upload port, speed and extra flags ; Library options: dependencies, extra library storages ; Advanced options: extra scripting ; ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html [env:spresense] platform = sonyspresense board = spresense framework = arduino build_src_filter = +<./> ; 自分のsrcフォルダをビルド対象に +<../external/doomgeneric/doomgeneric/> ; doomgenericフォルダもビルド対象に ; doomgeneric内の、他のプラットフォーム用ファイルを除外する -<../external/doomgeneric/doomgeneric/doomgeneric_allegro.c> -<../external/doomgeneric/doomgeneric/doomgeneric_emscripten.c> -<../external/doomgeneric/doomgeneric/doomgeneric_linuxvt.c> -<../external/doomgeneric/doomgeneric/doomgeneric_sdl.c> -<../external/doomgeneric/doomgeneric/doomgeneric_soso.c> -<../external/doomgeneric/doomgeneric/doomgeneric_sosox.c> -<../external/doomgeneric/doomgeneric/doomgeneric_win.c> -<../external/doomgeneric/doomgeneric/doomgeneric_xlib.c> -<../external/doomgeneric/doomgeneric/i_endoom.c> -<../external/doomgeneric/doomgeneric/i_allegromusic.c> -<../external/doomgeneric/doomgeneric/i_allegrosound.c> -<../external/doomgeneric/doomgeneric/i_sdlsound.c> -<../external/doomgeneric/doomgeneric/i_sdlmusic.c> -<../external/doomgeneric/doomgeneric/w_file_stdc.c> build_flags = -O3 ; 高速化を狙う その1 (最適化オプション Max) -flto ; 高速化を狙う その2 (リンク時最適化) -D __DJGPP__ ; 古いDOS開発環境用の定義。system で何もしなくなる -D USE_RGB565 ; RGB565 出力 -D DOOMGENERIC_RESX=320 ; 横解像度指定 -D DOOMGENERIC_RESY=200 ; 縦解像度指定 -I external/doomgeneric/doomgeneric -I src -include src/spresense_stdio_override.h lib_deps = lovyan03/LovyanGFX adafruit/Adafruit seesaw Library board_upload.maximum_ram_size = 1572864 board_upload.maximum_size = 1572864 upload_command = ${platformio.packages_dir}/tool-spresense/flash_writer/windows/flash_writer -s -c $UPLOAD_PORT -b $UPLOAD_SPEED -d -n -s $SOURCE upload_speed = 1000000

おわりに

パフォーマンスに難はありつつも、一応ゲームができるところまでいけました。今回はじめて DOOM のコードに触ってみたのですが、前情報通りプログラム設計がすばらしいと思いました。サウンドにも挑戦したいと思いつつ、残りのメモリが全然無いので、なかなか厳しいかもしれません。

メモリ使用状況、94.9%。

ビルドログ

#################################### ## Used memory size: 1536 [KByte] ## #################################### Checking size .pio\build\spresense\firmware.elf Advanced Memory Usage is available via "PlatformIO Home > Project Inspect" RAM: [======= ] 65.7% (used 1033268 bytes from 1572864 bytes) Flash: [=== ] 29.2% (used 460012 bytes from 1572864 bytes)
ログインしてコメントを投稿する