psxsdk

Split screens

A split screen is two pictures in one frame, each from its own camera. On the PlayStation it is two ordering tables: the scene is sorted into one with the GTE set to the first camera and into the other with it set to the second, and then each table is drawn into its half of the screen. The split example, a port of the SCEE tutorial's chapter 10 examples 20 and 21, gives each of two players a cube and a camera that follows it:

func main() {
	screen.Open()
	yaroze.GsInit3D()
	for i := range second {
		second[i] = yaroze.GsOT{Length: depth, Org: &secondTags[i<<depth]}
	}
	b := screen.Bounds()
	font := screen.Font(int32(b.X)+8, int32(b.Y)+8, int32(b.W)-16, int32(b.H)-16, 200)
	text.Init(textBuf[:])

	if f := disc.LoadAll(300, files...); f != nil {
		for range screen.Frames() {
			yaroze.Print("could not read ")
			yaroze.Print(f.Name)
		}
	}
	setUp()

	for {
		ot := screen.Begin()
		buf := yaroze.GsGetActiveBuff()
		yaroze.GsClearOt(0, 0, &second[buf])

		pad1.Update()
		pad2 = yaroze.ReadController(1)
		if pad1.Pressed(yaroze.PADselect) {
			sideBySide = !sideBySide
		}
		dx, dz := pad1.Dir()
		drive(&players[0], dx, -dz)
		dx, dz = faceDir()
		if pad2.Connected {
			if x, z := padDir(pad2.Buttons); x != 0 || z != 0 {
				dx, dz = x, z
			}
		}
		drive(&players[1], dx, -dz)

		// Each half: its camera, then the scene sorted into its table.
		for i, t := range [2]*yaroze.GsOT{ot, &second[buf]} {
			follow(&views[i], &players[i])
			views[i].Set()
			for j := range players {
				players[j].Draw(t)
			}
			for j := range stones {
				stones[j].Draw(t)
			}
		}

		text.Reset()
		text.WriteString("~c990PLAYER 1~c999")
		if sideBySide {
			text.WriteString("                ~c099PLAYER 2~c999\n")
		} else {
			text.WriteString("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n~c099PLAYER 2~c999\n")
		}
		text.Print()

		draw(ot, &second[buf], font)
	}
}

Screen.Begin gives the first half's table, and the program keeps the second half's, one for each display buffer as the screen's are.

Drawing each half#

Each half is drawn with its own drawing offset, the point on the screen the camera's centre lands on, the middle of the half, and its own clip, the rectangle drawing is kept inside. GsSetOffset and GsSetClip set both for the buffer being drawn, and the next GsSwapDispBuff puts them back:

// draw ends the frame as Screen.End does, but draws each half's table with
// that half's offset and clip, waiting for the GPU between them.
func draw(first, second *yaroze.GsOT, font int32) {
	w, h := int32(screen.Width), int32(screen.Height)
	yaroze.DrawSync(0)
	yaroze.VSync(0)
	yaroze.GsSwapDispBuff()
	// The clear is a fill, which no clip holds in: it clears the whole
	// screen though it is drawn with the first half.
	yaroze.GsSortClear(screen.Background.R, screen.Background.G, screen.Background.B, first)

	halves := [2]yaroze.RECT{
		{W: int16(w), H: int16(h / 2)},
		{Y: int16(h / 2), W: int16(w), H: int16(h / 2)},
	}
	if sideBySide {
		halves = [2]yaroze.RECT{
			{W: int16(w / 2), H: int16(h)},
			{X: int16(w / 2), W: int16(w / 2), H: int16(h)},
		}
	}
	for i, t := range [2]*yaroze.GsOT{first, second} {
		r := &halves[i]
		// The camera's centre goes to the middle of its half.
		yaroze.GsSetOffset(int32(r.X)+int32(r.W)/2, int32(r.Y)+int32(r.H)/2)
		yaroze.GsSetClip(r)
		yaroze.GsDrawOt(t)
		yaroze.DrawSync(0)
	}

	// The text across the whole screen, from its middle as GsInit3D set.
	whole := yaroze.RECT{W: int16(w), H: int16(h)}
	yaroze.GsSetOffset(w/2, h/2)
	yaroze.GsSetClip(&whole)
	yaroze.FntFlush(font)
	frames++
}

The two calls change the GPU's settings at once, not in turn with what it is drawing, and the library asks that the GPU not be busy when they are called. So the frame waits with DrawSync for the first half to be drawn before it sets up the second. The tutorial did not wait, which leaves the second half's settings to land in the middle of the first half's drawing, depending on how quickly the GPU gets through it.

The screen clear is a fill, which ignores the clip, so it is sorted into the first table and clears the whole screen. The text is drawn last, with the whole screen's offset and clip put back.

What it costs#

The scene is sorted twice, once for each camera, so a split screen takes twice the CPU and GTE work of one view, and each half has half the pixels to show it in. Two-player games drew less far, used simpler models, or ran at thirty frames a second instead of sixty. The timing page shows how to measure what a frame costs.

Select switches the example between halves one above the other and side by side. Side by side, each half is 160 pixels wide and the full height, and with the same projection it shows as much of the scene up and down but half as much across.