C++ Logo

STD-PROPOSALS

Advanced search

Subject: Re: [std-proposals] Remove infinite loop UB
From: Jason McKesson (jmckesson_at_[hidden])
Date: 2020-05-10 10:51:59


On Sun, May 10, 2020 at 11:04 AM connor horman via Std-Proposals
<std-proposals_at_[hidden]> wrote:
>
>
>
> On Sat, May 9, 2020 at 20:03 Thiago Macieira via Std-Proposals <std-proposals_at_[hidden]> wrote:
>>
>> On Friday, 8 May 2020 20:03:22 PDT connor horman via Std-Proposals wrote:
>> > SNES Game that's finished but needs still draw/redraw stuff. STP will mask
>> > the NMI from the VBLANK signal, which is how the game knows to load stuff
>> > into VRAM on the PPU, so it can be sent out on the multiout connector when
>> > the VBLANK is done.
>>
>> That counts like doing something other than idling in an infinte loop. Are
>> those requests handled by interrupts? The proper way of waiting in C++ would
>> be to sleep on a condition variable or something similar until everything is
>> done.
>
> Indeed, the Non-maskable Interrupt. And this continues until the system either is powered off, or a RESET occurs. I do still need to redraw the screen every Frame/VBLANK. The NMI Handler itself has to handle all of this, because its invalid to use dma, except during a VBLANK or FBLANK (which is set on entry to the NMI Handler, and cleared on exit). When the NMI returns, the main "thread" still has nothing to do but spin, waiting for the next one (or something more interesting, like an IRQ or ABORT interrupt).

I apologize if I get some things wrong. My knowledge of SNES
programming is primarily culled from a highly informative series of
YouTube videos (https://www.youtube.com/channel/UCwRqWnW5ZkVaP_lZF7caZ-g/videos),
so some of the following may be incorrect.

That having been said, I find this request rather confusing. During
standard execution of an SNES program, the typical thing that happens
is that the SNES CPU runs the main gameplay loop, then it spins and
waits for the NMI, and the NMI hijacks the CPU's execution and sends
it to what I will refer to as the VBlank Routine. This routine does
some PPU maintenance stuff for the next frame of video, then exits,
returning control to the exact position the CPU was in when it was
hijacked.

The NMI fires when the VBlank event happens, which is basically every
16ms. The job of the VBlank routine in normal gameplay is to take
information that the regular code has generated and turn it into
information that the PPU can use for display purposes. And if the
gameplay loop is not finished generating this data (ie: gameplay
processing took too long), then the VBlank routine needs to exit early
and return control to the gameplay loop so that it can to finish.

The typical way the gameplay loop signals that it has finished is by
setting a particular memory address to a specific value, and then it
sits in a loop, checking to see if this value has been changed. The
VBlank routine will change this value once it completes to indicate
that the gameplay loop can proceed to the next frame's processing. In
C++, the gameplay loop would therefore look like this:

```
//variable for VBlank
volatile uint8_t waiting_for_vblank = 0;

while(true)
{
  //Do normal gamplay processing

  //Spin to wait for VBlank routine
  waiting_for_vblank = 1;
  while(!waiting_for_vblank) {}
}
```

The VBlank routine will check to see if `waiting_for_vblank` is set to
1. If it is still zero, then that means the gameplay loop hasn't yet
started to spin, so processing took too long; the VBlank routine will
exit (and the PPU will redraw the same stuff). If `waiting_for_vblank`
is 1 at the start of VBlank, then the VBlank routine will do its job,
set it to 0, and return.

That's the normal way things work as I understand it.

What you're suggesting is that during the terminal phase of such a
program, there is no gameplay processing at all. Therefore, upon
entering such a state, the loop could just be `while(true) {}`. And
you want the standard changed to make that code legit.

Here's what confuses me.

You're in a state where there is no useful work to do in the main
routine. And we're not dealing with a multithreaded CPU, so other
routines that may exist do so by hijacking the main routine's
execution without its explicit knowledge. We're also talking about
hardware where stopping the CPU or doing nothing isn't an option. So
the only thing you can do in this routine is to *explicitly* do
nothing: execute an infinite loop.

OK, fine. My confusion comes from this question: why is busy loop 1
below better then busy loop 2 below:

```
//Busy loop 1:
while(true) {}

//Busy loop 2:
waiting_for_vblank = 0;
while(waiting_for_vblank) {}
```

The VBlank routine is coded to check to see if `waiting_for_vblank` is
1, and if it isn't, then it returns immediately. But that's fine,
because the VBlank routine *also* has no work to do; the PPU is just
redrawing the same stuff, which it can do on its own.

Nobody will set `waiting_for_vblank` to something other than zero, so
this loop will never terminate. And the loop has a side effect, so it
has well-defined behavior.

You could say that Busy loop 2 is slower than loop 1. After all, it
does a load from memory and a conditional branch rather than just a
branch.

But that doesn't matter because you're *idling*. You're literally
doing nothing; how long it takes to do nothing does not matter.

You didn't even need to set aside a new volatile variable for this;
you're just using the one you already had to have during primary
gameplay.

I don't know SNES assembly enough to know the difference, but can't be
more than like 6 bytes in terms of executable size. And again, the
performance is irrelevant because you're not doing anything useful.

So I'm not sure I understand the point of such a change.


STD-PROPOSALS list run by std-proposals-owner@lists.isocpp.org

Standard Proposals Archives on Google Groups