psxsdk

Timing

A game keeps time by its frames. The television draws a picture sixty times a second on an American or Japanese console and fifty on a European one, and a program draws one frame for each, or for every second one, so the frame is the clock everything else runs by: a sprite moves so many pixels a frame, a timer counts down so many frames. VSync(-1) returns how many fields have passed since the program started, which is a clock without anything else:

var rate int32 = 60

A program that wants seconds divides by the console's rate, which GetVideoMode says. A PAL game that took fifty frames to a second ran slow on an NTSC console's sixty, the other way round, and games converted for Europe often did run slow.

The root counters#

The CPU has four timers, the root counters, each a 16-bit count that runs up to 65,535 and round again:

const RCntCNT0 = 0xf2000000

const RCntCNT1 = 0xf2000001

const RCntCNT2 = 0xf2000002

const RCntCNT3 = 0xf2000003

StartRCnt starts one, ResetRCnt puts it back to zero and GetRCnt reads it. The library keeps the vertical blank's for itself. A program measures how long something takes by reading a counter before and after and taking the difference, which works across a wrap as long as the span is shorter than the counter's whole round: about four seconds of horizontal lines, or 15 milliseconds of the system clock.

How much of a frame#

VSync(1) returns the horizontal lines drawn since the last vertical blank, without waiting. Read when a frame's work is done, before the program waits for the next blank, it is how much of the frame the work took: the television draws 263 lines a field on NTSC and 313 on PAL, and work that takes more misses the next blank, so the program runs at half its rate until it takes less again. The timers example draws it as a bar, and up adds sprites until the bar fills:

func main() {
	screen.Open()
	screen.Font(16, 16, 288, 80, 300)
	text.Init(textBuf[:])
	if yaroze.GetVideoMode() == yaroze.MODE_PAL {
		frameLines, rate = 313, 50
	}

	if f := disc.LoadAll(300, files...); f == nil {
		sprite = yaroze.TIM(ballTIM.Data()).Load().Sprite()
	}
	yaroze.Srand(7)
	for i := range balls {
		b := &balls[i]
		b.x = int16(yaroze.Rand() % 300)
		b.y = int16(116 + yaroze.Rand()%100)
		b.dx = int16(yaroze.Rand()%5 - 2)
		b.dy = int16(yaroze.Rand()%5 - 2)
		if b.dx == 0 {
			b.dx = 1
		}
	}

	yaroze.StartRCnt(yaroze.RCntCNT1)
	yaroze.ResetRCnt(yaroze.RCntCNT1)
	frameBar = yaroze.GsBOXF{X: 16, Y: 92, W: 288, H: 6, R: 60, G: 60, B: 90}
	loadBar = yaroze.GsBOXF{X: 16, Y: 92, H: 6}

	for ot := range screen.Frames() {
		pad.Update()
		if n := pad.Duration(yaroze.PADLup); n == 1 || n > 15 {
			count = min(count+10, maxBalls)
		}
		if n := pad.Duration(yaroze.PADLdown); n == 1 || n > 15 {
			count = max(count-10, 0)
		}

		// The line counter, round and round.
		counter = yaroze.GetRCnt(yaroze.RCntCNT1)
		if counter > passing {
			wraps++
			yaroze.ResetRCnt(yaroze.RCntCNT1)
		}

		// The bar shows the last frame's work against the whole frame,
		// red once it is more than the frame holds. It is sorted first
		// so that it is drawn last, over the sprites: at one depth, the
		// last thing sorted is drawn first.
		loadBar.W = uint16(min(288, 288*lines/frameLines))
		loadBar.R, loadBar.G, loadBar.B = 80, 220, 120
		if lines > frameLines {
			loadBar.R, loadBar.G, loadBar.B = 240, 60, 40
		}
		loadBar.Sort(ot, 0)
		frameBar.Sort(ot, 0)

		for i := range count {
			b := &balls[i]
			b.x += b.dx
			b.y += b.dy
			if b.x < 0 || b.x > 304 {
				b.dx = -b.dx
			}
			if b.y < 116 || b.y > 224 {
				b.dy = -b.dy
			}
			sprite.X, sprite.Y = b.x, b.y
			sprite.Sort(ot, 0)
		}

		seconds := yaroze.VSync(-1) / rate
		text.Reset()
		text.WriteString("~c990LINE COUNTER~c999 ").WriteInt(counter)
		text.WriteString("\nPASSED ").WriteInt(passing).WriteString(" ").WriteInt(wraps).WriteString(" TIMES\n")
		text.WriteString("\n~c990CLOCK~c999 ").WriteInt(seconds / 60).WriteString(":")
		if seconds%60 < 10 {
			text.WriteString("0")
		}
		text.WriteInt(seconds % 60)
		text.WriteString("\n\n~c990FRAME~c999 ").WriteInt(count).WriteString(" SPRITES, ")
		text.WriteInt(lines).WriteString(" OF ").WriteInt(frameLines).WriteString(" LINES\n")
		text.Print()

		// The lines since the last vertical blank: this frame's work,
		// the sorting included, done.
		lines = yaroze.VSync(1)
	}
}

This is how Net Yaroze programmers found where their frames went, since the console had no profiler: time a part of the frame with a counter, or the whole of it with VSync(1), and draw the number.

The SCEE tutorial's chapter 5 is the original, three programs that this example puts together.