aboutsummaryrefslogtreecommitdiff
path: root/examples/beginner/hellocpp/main.cpp
blob: 20923be1d9913a412e476e87f3b224f6966e5b04 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
/*
 * PSn00bSDK C++ basic graphics example
 * (C) 2020-2023 Lameguy64, spicyjpeg - MPL licensed
 *
 * A C++ variant of the beginner/hello example showcasing the use of classes and
 * templates in place of structures, making the code more readable and less
 * error-prone. The OT and primitive buffer are now allocated on the heap and
 * automatically freed when the RenderContext class is destroyed or goes out of
 * scope.
 *
 * See the original example for more details.
 */

#include <cassert>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <psxgpu.h>

static constexpr size_t DEFAULT_OT_LENGTH     = 15;
static constexpr size_t DEFAULT_BUFFER_LENGTH = 8192;

/* RenderBuffer class */

class RenderBuffer {
private:
	DISPENV _disp_env;
	DRAWENV _draw_env;

	std::uint32_t *_ot;
	std::uint8_t  *_buffer;
	std::size_t   _ot_length, _buffer_length;

public:
	RenderBuffer(std::size_t ot_length, std::size_t buffer_length);
	~RenderBuffer(void);
	void setup(int x, int y, int w, int h, int r, int g, int b);

	inline uint8_t *buffer_start(void) const {
		return _buffer;
	}
	inline uint8_t *buffer_end(void) const {
		return &_buffer[_buffer_length];
	}
	inline uint32_t *ot_entry(int z) const {
		//assert((z >= 0) && (z < _ot_length));
		return &_ot[z];
	}

	inline void clear_ot(void) {
		ClearOTagR(_ot, _ot_length);
	}
	inline void draw(void) {
		DrawOTagEnv(&_ot[_ot_length - 1], &_draw_env);
	}
	inline void display(void) const {
		PutDispEnv(&_disp_env);
	}
};

RenderBuffer::RenderBuffer(std::size_t ot_length, std::size_t buffer_length)
: _ot_length(ot_length), _buffer_length(buffer_length) {
	// Initializing the OT in a constructor is unsafe, since ClearOTagR()
	// requires DMA to be enabled and may fail if called before ResetGraph() or
	// ResetCallback() (which can easily happen as constructors can run before
	// main()). Thus, this constructor is only going to allocate the buffers and
	// clearing is deferred to RenderContext::setup().
	_ot     = new uint32_t[ot_length];
	_buffer = new uint8_t[buffer_length];

	assert(_ot && _buffer);

	//std::printf("Allocated buffer, ot=0x%08x, buffer=0x%08x\n", ot, buffer);
}

RenderBuffer::~RenderBuffer(void) {
	delete[] _ot;
	delete[] _buffer;

	//std::printf("Freed buffer, ot=0x%08x, buffer=0x%08x\n", ot, buffer);
}

void RenderBuffer::setup(int x, int y, int w, int h, int r, int g, int b) {
	// Set the framebuffer's VRAM coordinates.
	SetDefDrawEnv(&_draw_env, x, y, w, h);
	SetDefDispEnv(&_disp_env, x, y, w, h);

	// Set the default background color and enable auto-clearing.
	setRGB0(&_draw_env, r, g, b);
	_draw_env.isbg = 1;
}

/* RenderContext class */

class RenderContext {
private:
	RenderBuffer _buffers[2];
	std::uint8_t *_next_packet;
	int          _active_buffer;

	// These functions are simply shorthands for _buffers[_active_buffer] and
	// _buffers[_active_buffer ^ 1] respectively. They are only used internally.
	inline RenderBuffer &_draw_buffer(void) {
		return _buffers[_active_buffer];
	}
	inline RenderBuffer &_disp_buffer(void) {
		return _buffers[_active_buffer ^ 1];
	}

public:
	RenderContext(
		std::size_t ot_length     = DEFAULT_OT_LENGTH,
		std::size_t buffer_length = DEFAULT_BUFFER_LENGTH
	);
	void setup(int w, int h, int r, int g, int b);
	void flip(void);

	// This is a "factory function" that allocates a new primitive within the
	// currently active buffer. It is a template method, meaning T will get
	// replaced at compile time by the type of the primitive we are going to
	// allocate (and sizeof(T) will change accordingly!).
	template<typename T> inline T *new_primitive(int z = 0) {
		// Place the primitive after all previously allocated primitives, then
		// insert it into the OT and bump the allocation pointer.
		auto prim = reinterpret_cast<T *>(_next_packet);

		addPrim(_draw_buffer().ot_entry(z), prim);
		_next_packet += sizeof(T);

		// Make sure we haven't yet run out of space for future primitives.
		assert(_next_packet <= _draw_buffer().buffer_end());

		return prim;
	}

	// A simple helper for drawing text using PSn00bSDK's debug font API. Note
	// that FntSort() requires the debug font texture to be uploaded to VRAM
	// beforehand by calling FntLoad().
	inline void draw_text(int x, int y, int z, const char *text) {
		_next_packet = reinterpret_cast<uint8_t *>(
			FntSort(_draw_buffer().ot_entry(z), _next_packet, x, y, text)
		);

		assert(_next_packet <= _draw_buffer().buffer_end());
	}
};

RenderContext::RenderContext(std::size_t ot_length, std::size_t buffer_length)
: _buffers{
		RenderBuffer(ot_length, buffer_length),
		RenderBuffer(ot_length, buffer_length)
} {}

void RenderContext::setup(int w, int h, int r, int g, int b) {
	// Place the two framebuffers vertically in VRAM.
	_buffers[0].setup(0, 0, w, h, r, g, b);
	_buffers[1].setup(0, h, w, h, r, g, b);

	// Initialize the first buffer and clear its OT so that it can be used for
	// drawing.
	_active_buffer = 0;
	_next_packet   = _draw_buffer().buffer_start();
	_draw_buffer().clear_ot();

	// Turn on the video output.
	SetDispMask(1);
}

void RenderContext::flip(void) {
	// Wait for the GPU to finish drawing, then wait for vblank in order to
	// prevent screen tearing.
	DrawSync(0);
	VSync(0);

	// Display the framebuffer the GPU has just finished drawing and start
	// rendering the display list that was filled up in the main loop.
	_disp_buffer().display();
	_draw_buffer().draw();

	// Switch over to the next buffer, clear it and reset the packet allocation
	// pointer.
	_active_buffer ^= 1;
	_next_packet    = _draw_buffer().buffer_start();
	_draw_buffer().clear_ot();
}

/* Main */

static constexpr int SCREEN_XRES = 320;
static constexpr int SCREEN_YRES = 240;

int main(int argc, const char **argv) {
	// Initialize the GPU and load the default font texture provided by
	// PSn00bSDK at (960, 0) in VRAM.
	ResetGraph(0);
	FntLoad(960, 0);

	// Set up our rendering context.
	RenderContext ctx;
	ctx.setup(SCREEN_XRES, SCREEN_YRES, 63, 0, 127);

	int x  = 0, y  = 0;
	int dx = 1, dy = 1;

	for (;;) {
		// Update the position and velocity of the bouncing square.
		if (x < 0 || x > (SCREEN_XRES - 64))
			dx = -dx;
		if (y < 0 || y > (SCREEN_YRES - 64))
			dy = -dy;

		x += dx;
		y += dy;

		// Draw the square by allocating a TILE (i.e. untextured solid color
		// rectangle) primitive at Z = 1.
		auto tile = ctx.new_primitive<TILE>(1);

		setTile(tile);
		setXY0 (tile, x, y);
		setWH  (tile, 64, 64);
		setRGB0(tile, 255, 255, 0);

		// Draw some text in front of the square (Z = 0, primitives with higher
		// Z indices are drawn first).
		ctx.draw_text(8, 16, 0, "Hello from C++!");

		ctx.flip();
	}

	return 0;
}