Game Boy Advance 开发:控制台日志记录
Game Boy Advance Dev: Logging to the Console

原始链接: https://www.mattgreer.dev/blog/gba-dev-logging/

在开发 Game Boy Advance 游戏时,可以使用 mGBA 的内存映射日志寄存器来实现类似 `printf` 的调试功能。通过向特定的内存地址(`REG_LOG_ENABLE`、`REG_LOG_BUFFER` 和 `REG_LOG_SEND`)写入数据,你可以将调试信息直接输出到模拟器的日志窗口或终端。 要实现此功能,请创建一个辅助函数,用于格式化信息并将其写入这些寄存器。为方便起见,你可以结合 `vsnprintf` 来支持标准的 C 语言格式化(例如 `mgbalog(DEBUG, "x=%i", x);`)。 由于记录日志会产生性能和内存开销,因此应将其从最终版本中排除。你可以通过将日志实现代码包裹在 `#ifdef MGBALOG` 代码块中,并为 `mgbalog()` 函数使用预处理器宏来实现这一点。这样,你既可以在代码库中保留日志调用,又能确保在禁用 `MGBALOG` 标志时将它们完全从编译中剔除。这种简单的设置可以显著简化调试过程,且不会影响游戏的最终发布。

抱歉。
相关文章

原文

Printing to the console with things like printf() in C or console.log() in JavaScript is a useful tool while developing. When making a GBA game, you might think you have to forego this, or log to the GBA's tiny little screen. If you use mGBA, you can get printf() functionality in your game. It's simple to do.

How it works

You often write to registers like REG_DISPCNT when doing GBA dev. This is a memory mapped register, and it's a simple way for your game to tell the GBA hardware what you want it to do. This is just an address in the GBA's memory map, and when it is written to, the GBA hardware will take the data it receives and act accordingly.

mGBA added a few more memory mapped registers for the purpose of logging. They work just like REG_DISPCNT and the like, except they only work when your game is running inside mGBA.

Here are the definitions for these mGBA specific registers.

#define REG_LOG_ENABLE (vu32*)(0x4FFF780)

#define REG_LOG_BUFFER (vu32*)(0x4FFF600)

#define REG_LOG_SEND   (vu32*)(0x4FFF700)

So to log some data, you first send a special value to REG_LOG_ENABLE to turn logging on. Then you write your data to REG_LOG_BUFFER. Then when you are done, write the log level to REG_LOG_SEND and at that point mGBA will take the data you sent it and output it to its log.

This sounds a little complicated, and it kind of is. But you really just hide all of these details inside of a function and never think about them again. Here is that function.

#define REG_LOG_ENABLE (vu32*)(0x4FFF780)

#define REG_LOG_BUFFER (vu32*)(0x4FFF600)

#define REG_LOG_SEND   (vu32*)(0x4FFF700)

#define MGBA_LOG_MAX_LINE 256

#define ERROR 0x101

#define WARNING 0x102

#define INFO 0x103

#define DEBUG 0x104

void mgbalog(u32 level, const char *msg) {

  *REG_LOG_ENABLE = 0xC0DE;

  tonccpy((void *)REG_LOG_BUFFER, msg, MGBA_LOG_MAX_LINE);

  *REG_LOG_SEND = level;

}

Then you can use it in your game whenever you need to log something.

#include "mgbalog.h"

void myFunc() {

    ...

    mgbalog(DEBUG, "hello mGBA log");

}

When you run the game, go to Tools > View logs... to open up the log window.

The mGBA log window with our log message in it.

And there's your message.

When logging, you can choose to log at various levels: error, warning, info or debug. That's what the level business is for in mgbalog(). You write your desired log level to REG_LOG_SEND, this pulls double duty of telling mGBA at what level to store the message, and that you're done writing the message so mGBA can now process it.

Logging to your terminal

Opening the log window every time to view your logs can be tedious. If you launch mGBA from the command line, you can also tell it to write the logs to your terminal. To do this, go into the log window again and click, on advance settings, then check "Log to console" at the bottom.

Log to console checked in the logging advance settings window.

mGBA itself logs a lot of stuff. You can turn it all on/off in this dialog as well.

Now if you launch your game via the command line, the log output will show up there as well. You can control which level of log you want with --log-level. For example, here is how to only log DEBUG to the console.

mgba --log-level 16 yourRom.gba

Here are the log levels

  • 1 - fatal errors
  • 2 - errors
  • 4 - warnings
  • 8 - info
  • 16 - debug
  • 32 - stub
  • 64 - in-game errors

And you can combine them, so if you want debug and error, do --log-level 18

Adding formatting

Logging a static string is rarely useful. Thankfully it is easy to add printf style formatting.

My blog doesn't allow C includes which use < and >, so I added spaces to work around that. I really need to fix that...

#include stdarg.h >

#include stdio.h >

static char logBuffer[MGBA_LOG_MAX_LINE];

void mgbalog(u32 level, const char *format, ...) {

  *REG_LOG_ENABLE = 0xC0DE;

  va_list formatArgs;

  va_start(formatArgs, format);

  vsnprintf(logBuffer, MGBA_LOG_MAX_LINE, format, formatArgs);

  va_end(formatArgs);

  tonccpy((void *)REG_LOG_BUFFER, logBuffer, MGBA_LOG_MAX_LINE);

  *REG_LOG_SEND = level;

}

Now you can do things like mgbalog(DEBUG, "pos.x=%i", pos.x);.

Only logging during development

Logging adds to your binary, uses memory and cpu, and is generally not wanted in the final version of your game. This is especially true if you add formatting, things like vsnprintf from the standard library are pretty expensive on the poor lil GBA.

I hide my logging implementation behind an MGBALOG define. So when I want logging, I define MGBALOG. If it's not defined, nothing related to logging will get added to the binary.

#ifdef MGBALOG

void mgbalog(u32 level, const char *format, ...) {

    ...

}

#endif

That is all well and good, but this means if you don't define MGBALOG, then you have to remove all logging calls throughout your codebase, otherwise you will get a compilation error.

This is easily fixed with more define schenanigans. This is my mgbalog.h

#pragma once

#include tonc.h >

#define MGBA_LOG_MAX_LINE 256

#define ERROR 0x101

#define WARNING 0x102

#define INFO 0x103

#define DEBUG 0x104

#define REG_LOG_ENABLE  (vu32*)(0x4FFF780)

#define REG_LOG_BUFFER (vu32*)(0x4FFF600)

#define REG_LOG_SEND (vu32*)(0x4FFF700)

#ifdef MGBALOG

void _mgbalog(u32 level, const char *format, ...);

#define mgbalog(...) _mgbalog(__VA_ARGS__)

#else

#define mgbalog(level, format, ...)

#endif

Then throughout my game I call mgbalog(...) as before whenever I want to log something. This is now a macro that either calls _mgbalog() if MGBALOG is defined, or does nothing at all when it is not defined. Now you don't need to worry about your log calls, the game will always compile regardless.

With the above header file, here is my implementation.

#ifdef MGBALOG

#include "mgbalog.h"

#include stdarg.h >

#include stdio.h >

static char logBuffer[MGBA_LOG_MAX_LINE];

void _mgbalog(u32 level, const char *format, ...) {

  *REG_LOG_ENABLE = 0xC0DE;

  va_list formatArgs;

  va_start(formatArgs, format);

  vsnprintf(logBuffer, MGBA_LOG_MAX_LINE, format, formatArgs);

  va_end(formatArgs);

  tonccpy((void *)REG_LOG_BUFFER, logBuffer, MGBA_LOG_MAX_LINE);

  *REG_LOG_SEND = level;

}

#endif

Conclusion

That's all there is to it. A simple little bit of code adds a lot of utility to your dev experience. There are mgba logging libraries out there if you don't want to roll your own. Or if you are using an engine like Butano, it already has mgba logging built in. Now when using a logging library, you know what it is doing behind the scenes, which is always a good thing.

联系我们 contact @ memedata.com