Skip to content

Latest commit

 

History

History
194 lines (150 loc) · 18.5 KB

Chapter 3.md

File metadata and controls

194 lines (150 loc) · 18.5 KB

Wii U Programming

A quick thrash for existing programmers

Chapter 3 - Hello, world!

Alright, let's get into it! We're going to look at a very simple wut application that uses two major libraries - libwhb and OSScreen. We'll take a quick look at the structure of a wut application, the headers you might need, setting up logging and graphics, and some of libwhb's helpers to manage your app's lifecycle.

Open up resource 3.1 in another tab - that's the code we'll be referencing throughout this chapter. We've only got two files there - CMakeLists.txt and main.c. CMakeLists.txt is part of the wut build system - as you've probably guessed, wut uses cmake to build homebrew, using macros to add a few extensions we'll look at later. main.c is the only source file for this app, and it's what we'll look at first in this chapter.

You might also want to reference against wut's documentation, which lists all the Nintendo-provided functions along with descriptions for most of them. I'll link to the specific functions where I can, though be aware the links may go bad as wut updates.

main.c - line by line

So, let's open up this source and have a look! Feel free to take this at your own pace - read the source, read this chapter, whatever. I've added some comments to main.c if you want to blow through quickly, but for a deeper look I'm going to go over it line by line.

#include <stdint.h>
#include <stdbool.h>
#include <malloc.h>

This is about what you'd expect from a C program. There are a few small considerations when using the standard C functions - we'll get to those later.

#include <coreinit/screen.h>
#include <coreinit/cache.h>
#include <whb/log_cafe.h>
#include <whb/log_udp.h>
#include <whb/log.h>
#include <whb/proc.h>

Here's our first hint that this is no average C program! <coreinit/screen.h> is a wut-supplied header that contains all the OSScreen functions we'll be using later to make some graphics. <coreinit/cache.h> provides functions to manage the processor cache - we'll look at these in detail when we use them. The other three headers are from libwhb, pulling in logging (with both Cafe and UDP backends - more on this later) and proc.h, which we'll also come to soon enough.

int main(int argc, char** argv) {

Homebrew using wut has a normal main function, as you'd expect. One thing to note is that there's not an obvious way for the user to influence the argc/argv values, so there's little point in parsing them in most cases.

WHBLogCafeInit();
WHBLogUdpInit();
WHBLogPrint("Hello World! Logging initialised.");

Here we set up some logging! libwhb has this neat system where you can enable several logging backends, and your messages will get sent to all of them. First, we call WHBLogCafeInit to set up libwhb's Cafe logger - this uses the Wii U's internal logging systems. These aren't usually visible to you, but the messages do show up when using decaf-emu (a Wii U emulator) or when viewing certain crash logs on-console. Next, we call WHBLogUdpInit to init the UDP logger - it'll send all log messages out over the network. These can be recieved with the udplogserver tool, included with wut. We can now use WHBLogPrint to send out our first message!

libwhb also ships with a "Console" logging backend, which prints your log messages to the screen. For the purposes of this tutorial, we're going to handle printing to the screen ourselves! We'll have a quick look at the Console backend later so you can save time for simple projects. In any case, let's continue with the code.

WHBProcInit();

While it might not be obvious at first glance, Cafe is actually a fully multitasking OS, with a concept of the "foreground" and the "background". Sometimes, our app will get moved into the background - this can happen to open the HOME menu overlay, for example; though apps like the Internet Browser or Nintendo eShop do the same thing. Nintendo provides a library called ProcUI to handle these transitions along with a few other functions - the auto power-down features, for example. While ProcUI is relatively simple, libwhb provides an even simpler wrapper so we don't really have to think about it at all. Here we initialise that.

Note: Even though libwhb handles most ProcUI functionality, our code still has to be somewhat modified to allow moving in and out of the background. For the sake of simplicity, the graphics code we're going to use makes no attempt to do this, so things like the HOME menu overlay are unlikely to work right. Also note that when running from HBL, libwhb will exit the app instead of moving to the background.

OSScreenInit();

Here we start to init OSScreen; a really simple graphics library that Nintendo made (though we've never seen them use it). Just calling OSScreenInit isn't quite enough, though - most of what we're doing from here on out will be related to OSScreen in some way.

size_t tvBufferSize = OSScreenGetBufferSizeEx(SCREEN_TV);
size_t drcBufferSize = OSScreenGetBufferSizeEx(SCREEN_DRC);
WHBLogPrintf("Will allocate 0x%X bytes for the TV, " \
             "and 0x%X bytes for the DRC.",
             tvBufferSize, drcBufferSize);

OSScreen needs two memory areas to put framebuffers in - here we use OSScreenGetBufferSizeEx to ask how much it needs, both for the TV and DRC (Gamepad). We'll need access to these sizes even after allocation for reasons we'll see later, so I stick them in variables.

void* tvBuffer = memalign(0x100, tvBufferSize);
void* drcBuffer = memalign(0x100, drcBufferSize);

Now we can actually allocate! There's nothing too noteworthy here, except my choice of memalign over a normal malloc - we'll get to my reason for this in a moment.

Note: allocations and local variables do not necessarily start zeroed on Cafe like they do on other platforms - make sure you account for this or use calloc!

I'm going to skip over the nullcheck here - you can go look at it in the code if you want, but rest assured it's just an if (!tvBuffer || !drcBuffer) followed by the shutdown code we'll look at later.

OSScreenSetBufferEx(SCREEN_TV, tvBuffer);
OSScreenSetBufferEx(SCREEN_DRC, drcBuffer);

So, we've allocated the memory OSScreen asked for, so now we just have to tell it where it is! This code should be pretty self-explanatory - we use OSScreenSetBufferEx to set the pointers for both the TV and DRC screens. If you check out the linked documentation for that function, you'll note how it stresses the pointers should be 0x100 aligned - this is the reason I went with memalign before.

OSScreenEnableEx(SCREEN_TV, true);
OSScreenEnableEx(SCREEN_DRC, true);

With that, we've got OSScreen set up and good to go! We call OSScreenEnableEx to have OSScreen actually take control of the displays and get our framebuffer up.

With all our initiaisation code out of the way, it's time to get on with our planetary greetings!

while(WHBProcIsRunning()) {

I talked before about what libwhb's ProcUI wrapper does - managing foreground/background states, some system poweroff features, whether we need to quit, things like that. All that processing actually happens within this call - WHBProcIsRunning calls out to Nintendo's ProcUI library, handles any requested foreground/background transitions, then finally returns true or false depending on whether the app should keep running. Due to the nature of the work it does, it makes sense to call this semi-regularly - so I elected to use it for a while loop in this example.

OSScreenClearBufferEx(SCREEN_TV, 0x00000000);
OSScreenClearBufferEx(SCREEN_DRC, 0x00000000);

So, we're inside our loop - it's time to start rendering! We start with calls to OSScreenClearBufferEx for both the TV and Gamepad - this will wipe both framebuffers clean, filling them with the colour we request. Here, we pass in 0x00000000 - this is RGBX format colour. That means the first two digits are the red value, the next two are green, the third two are blue and the last two are ignored. All the values range from 00 to FF. So, this colour has 00 red, 00 green, and 00 blue... sounds like black! Making both screens black gives us a convenient starting point to render on top of.

OSScreenPutFontEx(SCREEN_TV, 0, 0, "Hello world! This is the TV.");
OSScreenPutFontEx(SCREEN_TV, 0, 1, "Neat, right?");

OSScreenPutFontEx(SCREEN_DRC, 0, 0, "Hello world! This is the DRC.");
OSScreenPutFontEx(SCREEN_DRC, 0, 1, "Neat, right?");

Now we've cleared out our framebuffer, we can render some text into it! This is done with OSScreenPutFontEx - it takes a screen, a row and column to start the text at; measured in characters, not pixels; and finally the string to actually draw. Here we put the text "Neat, right?" on the line below the "Hello World!" text - that third parameter is the row the text should start on.

One thing to keep in mind when using OSScreenPutFontEx is that the TV runs at a higher resolution than the Gamepad. OSScreen runs the TV at 720p, with the result being scaled to the actual system resolution later. The Gamepad is always 480p, so you need to be careful - even if your text looks fine on the TV, it might be outside the borders of the Gamepad! There are ways to stretch the TV image so everything matches, but that's outside the scope of this tutorial.

At this point, you might think we're done - We've rendered our text, right? Sadly, we're not quite there yet - there's two things that remain to be done: flushing and flipping the buffers.

DCFlushRange(tvBuffer, tvBufferSize);
DCFlushRange(drcBuffer, drcBufferSize);

It'd be crazy to try and fully explain everything that's going on here in a few short paragraphs. For those of you who are good with your CPU internals, this function flushes the buffers since the Wii U hardware isn't cache-coherent. You can probably skip to the next snippet now! For the rest of us, let's dive in.

If you remember the recap of computer science from Chapter 1, you'll know that all our data and code is stored in memory. The thing is, memory is slow. Compared to the CPU, it's outright unbearable. If the CPU interfaced with memory directly, it could easily spend most of its time waiting on the memory to cough up the required data. To solve this problem, modern computers have a cache - A small area of memory, usually inside the CPU chip itself. Despite its size - 512KiB on the Wii U (one of the three cores has 2MiB) - this memory is blazingly fast. You can't access it directly though; instead the CPU manages it, keeping a copy of any recently-accessed memory there. If the CPU needs to use that same memory again, it can get the data from the cache instead of the much slower main memory! It can also save some time by writing memory changes to the cache and letting the changes "filter down" to main memory in their own time - this is useful for things like counters that change frequently but don't really need to be in main memory.

This is great and results in a much faster system. There is one problem though - since the CPU can write to the cache without actually updating main memory, all our OSScreen rendering efforts may be caught up in the cache while memory remains outdated. This isn't an issue for our code on the CPU - we'll always read the most recent data - but the GPU is a different story. It knows nothing of the CPU's caches and just wants to read our pixel data from main memory. We can ensure that our writes from the CPU are actually in memory by flushing the cache - any changes stuck in the cache are written out and we can rest assured that main memory actually contains the data we wrote. That's why we need to call these functions - we flush any pending writes out of the data cache (data cache flush range, or DCFlushRange), so we can know that the GPU can read our image correctly and display what we want it to.

With that out of the way...

OSScreenFlipBuffersEx(SCREEN_TV);
OSScreenFlipBuffersEx(SCREEN_DRC);

We've been drawing plenty of text into the OSScreen framebuffers, but if we stopped right here none of it would actually show up on screen. That's because all our text and image data has been drawn into the work buffer - a secondary framebuffer that everything gets drawn to. The screen doesn't show the work buffer, instead displaying the visible buffer. When we're ready, we can swap these two buffers by calling OSScreenFlipBuffersEx - the work buffer becomes the visible buffer and is shown on-screen, while the old visible buffer becomes our new work buffer. This is known as "flipping" the buffers. This system is used to avoid screen tearing and visual glitches while we draw - things are only shown on-screen once we're ready to show them. It also gives us a chance to sort out the caches! This system of flipping two buffers is widely known as "double buffering".

}
WHBLogPrint("Got shutdown request!");

At this point, our text is on-screen! Marvel at it in all its glory. The while loop we set up before will continue looping, constantly drawing new frames and displaying them. Pressing the HOME button will make WHBProcIsRunning return false, ending the loop and telling us to clean up and quit.

if (tvBuffer) free(tvBuffer);
if (drcBuffer) free(drcBuffer);

We allocated these buffers earlier, so it's essential that we free them as part of our cleanup process. While PC applications can get away with being a tad lax here, the Wii U is a different story - depending on how your application ends up getting launched, it's possible for memory leaks to stick around after your application exits, sapping memory space from other programs. So, we play good citizen and free our buffers here.

Note: Depending on which allocator your app uses (more on this later), calling free(NULL) can result in an application crash. To be safe, always do a null-check before freeing.

OSScreenShutdown();
WHBProcShutdown();

WHBLogPrint("Quitting.");
WHBLogCafeDeinit();
WHBLogUdpDeinit();

Not much to say here - we simply de-init the libraries we were using. Again, it's essential that we do this thoroughly to prevent knock-on effects to other applications.

    return 1;
}

Well, that's it! We return a sane value here (some significance may be assigned to them in future) and our application quits!

Note: Returning -3 is reserved for HBL - in most cases, returning this will cause Mii Maker to open.

A Very Quick Compilation Run-Through

Now we've got the code in order and ready for use, it's time for compiling. Covering the whole range of setups and possibilities for compiling is well outside the scope of this tutorial, so I'm gonna assume you've got that sorted in your own time. CMake builds are pretty well standardised across platforms and whatever, so assuming you're on a UNIXy system - that's Linux, macOS, OSX, msys2, WSL/Ubuntu on Windows, and, well, basically anything that's not Windows' standard command processor (I belive devkitPro ships with msys2) then this should look very familiar to you:

# Change into the folder where CMakeLists.txt is located
cd resources/3-1-HelloWorld
# Make a directory to build in - this is called out-of-tree building
mkdir build
cd build
# Run cmake, pointing it to the parent (where CMakeLists.txt is) - this will
# generate a Makefile
cmake ..
# Run make to actually build the app
make

Once you've done that, you should get helloworld.rpx in your build folder! This can be sent to HBL with tools like wiiload or dropped on your SD somewhere like sd:/wiiu/apps/helloworld/helloworld.rpx (fun fact: HBL doesn't actually need a meta.xml to show an app). Once running, you can run udplogserver to show the messages sent through WHBLogPrintf. Take a moment to run the app and see it in action!

Suggestions and Further Learning

Once you've gotten over your obvious excitement (it didn't crash!) I recommend you go download a copy of the code (resource 3.1) and mess with it. Tweak things, move chunks of code around, add new calls and logic, whatever - run make again after each change, and try the new code on-console. While you might break the app, I assure you it's impossible to break your console with OSScreen - if the worst happens and your code becomes unresponsive, just hold the power button down until the console switches off (the power LED will turn red). Get used to that - You'll probably be doing it a lot!

Here's some ideas for things you might change:

  • Change the text that gets drawn onscreen - change some words, maybe write a whole new message? Add a few extra lines of text. How much can you write before going off the side of the screen?
  • Change the background colour. Red or blue is easy - how about yellow? Purple? Try a different colour on the TV and the Gamepad.
  • Remember OSSleepTicks, from Chapter 2? Try adding a small delay at the end of the while loop - how long can you wait before controls start becoming unresponsive? Don't forget your #includes.
  • Play around with some of the other OSScreen functions - how about OSScreenPutPixelEx? Try drawing some shapes with that.

It may seem somewhat silly, but this is how I learnt and how most of the other Wii U developers I talked to learnt - taking existing code and experimenting on it; changing it to do whatever they want it to do. If you get stuck, feel free to ask for help! We've all been there and will gladly help. Wherever you found out about this tutorial is probably a good place to try.

Chapter 3: So long! In Chapter 4, we'll button-mashing Gamepad action! Take a look here.