Wiki

Clone wiki

agtools / Tutorial 1 - The Shifter Display Interface

Outline:

ts1.png

tutorials/tutor1

IF YOU JUST WANT TO START USING AGT ASAP, SKIM THIS AND/OR SKIP TO TUTORIAL 2

You can build this example with:

> make clean; make

This example should be more fun than tutor0 - we're going to put 'something' on the screen. It won't be anything exciting, but it does introduce the shifter interface which helps us to manage the display side of graphics framebuffers.

shifter_ste shift;

As was the case for the 'machine' interface, the shifter interface is defined as a single named instance of the 'shifter' class. This will provide global access to display states.

Next the program declares space for the front and back framebuffer (or physic, logic if you prefer). This is done in a simple, naive way - not the recommended way! It's the C equivalent to 'ds.b <size>' in a 68k asm program. We do this here to keep the sample short an easy to follow.

#define SCREEN_LINES (240)

// define word buffers for a couple of hardcoded screens. add a single column of +16 to accommodate hscroll.
u16 screenbuffer0[SCREEN_LINES * ((320+16)/4)];
u16 screenbuffer1[SCREEN_LINES * ((320+16)/4)];

// an array of 2 pointers, one for each screenbuffer. used to manually select between them using index 0 or 1.
u16* screenbuffer_ptrs[] =
{
    screenbuffer0,
    screenbuffer1
};

With this we can locate front/back via screenbuffer_ptrs[choice] where choice = 0 or 1. We'll use this for page flipping.

Since this is a real AGT program which will access STE features, we detect the machine type and bail out early if not satisfied.

    if (!bSTE && !bMegaSTE && !bFalcon030)
    {
        ...bail out...
    }
    else
    {

...and now the AGT program begins properly.

Hardware init:

First, we save the machine state. Note that .configure() was already called beforehand.

        machinestate.save();

Next we configure the shifter interface, and save its own specific state. As before - configure() first (once only) and then save() before using.

        shift.configure();
        shift.save();

Next we make our claim on the machine state:

        machinestate.claim();

This does a number of things, but in summary it turns off anything which will interfere with display or timing system. It turns the lights out, before we start using our own services.

Shifter init:

The following two lines prepare the shifter interface for displaying a framebuffer.

        shift.set_fieldmode(/*dualfield=*/false);
        shift.init();

The 'fieldmode' parameter selects single- or dual-field display. We'll stick with single-field for the early tutorials. More on dual-field later (it's a kind of colour interlacing mode which boosts colour availability).

The init() call starts the display service itself. This installs a new vblank handler and other details needed to support our 320x240 display resolution.

Next we load our palette into the shifter, with a call to setcolour() for each of 16 colours, for each of the two framebuffers (front and back).

        shift.setcolour(/*shiftgroup=*/0, /*shiftfield=*/0, /*colnum=*/c, /*col=*/rgb);

The first argument shiftgroup is the buffer number (i.e. front or back). The second shiftfield should remain 0 for single-field graphics. colnum is colour index 0-15 and col is the STE palette colour value to assign.

But before racing past this step, we should spend a bit of time on whats going on here because it reveals the reason for having a shifter interface at all (versus just poking the hardware registers).

The shifter interface holds a complete, distinct hardware state for each memory buffer to be displayed.

This hardware state includes:

  • screen address
  • screen linewidth
  • scroll position
  • palette

We need to be a bit careful here because the term 'memory buffer' is not necessarily the same thing as 'front/back buffer'. Why? Because AGT supports interlaced-colour display (dual-field mode) where each buffer (front or back, or more if required) has an odd and even 'field'. Each field requires its own distinct memory buffer and shifter state.

We're going to skip over dual-field for now (thankfully!), but it's important to realise the 'front/back' concept doesn't select between literal memory buffers. It selects between two 'shiftgroups' - the active one and the pending one (next to be presented). Each shiftgroup has two 'shiftfields' - one odd, one even. When dual-field is disabled, only one of these gets allocated a memory buffer and gets used - so in most respects each shiftgroup acts like a single buffer anyway, and we'll pretend that's the case for the tutorial.

Dual-field aside, the second reason for holding a distinct shifter state for each memory buffer is software synchronisation. Ideally all relevant hardware registers will be loaded at one specific point in time (and the correct point in time). We don't want the screen address set now, and the scroll position follows 1+ vblanks later - so it should naturally be handled by the shifter service - not the game code or mainloop. But equally important is making sure the correct state is loaded - the one relating to the graphics just drawn to the backbuffer. If the backbuffer swap is delayed for some reason (say a framerate drop to 25hz) you want the shifter state to be delayed as well - otherwise the display will get seriously messed up.

So the shifter interface provides binding of display states (e.g. scrolling & palette) with the correct framebuffer by decoupling the hardware states from the running program / mainloop. The state is assembled during each mainloop pass, taking as long as necessary and then committed all at once. The committed changes will take effect on the next vblank to occur. We'll see how that is achieved next.

Test image:

Next we put some stuff in the front and back buffer. The details of this don't matter too much - its just a bitmap pattern of columns.

    screenbuffer0[dst] = 0xF000;
    ...etc

Mainloop:

Next we declare a few variables for use in the mainloop.

The important one here is our buffer index, which implements our front/back page flip on each refresh.

    s16 backbuffer_num = 0;         // index of current backbuffer (0 or 1)

We then configure our mainloop for 256 refreshes (before exiting) - since we don't have a keyboard service installed yet.

The mainloop itself first implements a wait-vbl by polling the global g_vbl variable until it changes. This variable gets updated by the shifter service. In a later example we'll see how to run the mainloop as a foreground thread instead, for specific benefits.

Then we provide some dummy work - in this case its a block of random colour changes, consuming a bunch of CPU time.

    for (s32 flash = 0; flash < 1000; flash++)
    {
        reg16(FFFF8240) += 0x237; 
    }

Displaying:

Finally, we get to the interesting bit - the point of the exercise. Using the shifter interface to present the framebuffer with its complete hardware state.

First, get a pointer to the current backbuffer

    u16 *backbuffer_ptr = screenbuffer_ptrs[backbuffer_num];

Then load the backbuffer's state into the shifter, specifying the dimensions and scroll offsets at once. Note that we're keeping the first 'field' argument at 0 since we're using a single-field display. We also specify the virtual width of the framebuffer as 320+16 to accommodate the 16 pixel hidden margin assumed by STE hardware scrolling.

Note: The physical screen starts inside the top border, but the first visible line is around 16 lines later. The lines in-between have been blanked out to balance the screen height to 240, centered. So we prepare 256 lines and ignore the top 16. We only have to deal with this because we're doing everything with screens manually here.

    shift.setdisplay(0, (u32)backbuffer_ptr, 320, 320+16, xscroll, yscroll);

Now our changes so far won't 'take' unless we advance the shifter's internal state. Until then, the changes are merely pending.

    shift.advance();

To complete the effect, we slide the screen towards the left by incrementing the xscroll variable (looping at 16 pixels).

    xscroll = (xscroll + 1) & 15;

Note that while we're wrapping at 16 pixels - larger xscroll values will be accepted and will translate to the correct physical screen address + scroll state. In other words - the xscroll parameter is a full pixel x-coordinate, not a STE scroll field. We're wrapping it because we've only reserved memory for a (320+16) wide image!

Last of all - assuming double-buffering is required (not really for this demo - but likely for most cases), swap the front/back buffer over for the next refresh.

    backbuffer_num = backbuffer_num ^ 1;

Summary:

This tutorial described how to implement double-buffered, scrolling graphics using the AGT shifter interface alone. It's not exactly a stunning demo, but the next tutorials will build upon it and the utility of the shifter will become more clear.

Updated