1 | // |
---|
2 | // Lol Engine |
---|
3 | // |
---|
4 | // Copyright: (c) 2010-2012 Sam Hocevar <sam@hocevar.net> |
---|
5 | // This program is free software; you can redistribute it and/or |
---|
6 | // modify it under the terms of the Do What The Fuck You Want To |
---|
7 | // Public License, Version 2, as published by Sam Hocevar. See |
---|
8 | // http://sam.zoy.org/projects/COPYING.WTFPL for more details. |
---|
9 | // |
---|
10 | |
---|
11 | #if defined HAVE_CONFIG_H |
---|
12 | # include "config.h" |
---|
13 | #endif |
---|
14 | |
---|
15 | #include <cstdlib> |
---|
16 | #include <stdint.h> |
---|
17 | |
---|
18 | #include "core.h" |
---|
19 | |
---|
20 | namespace lol |
---|
21 | { |
---|
22 | |
---|
23 | /* |
---|
24 | * Ticker implementation class |
---|
25 | */ |
---|
26 | |
---|
27 | static class TickerData |
---|
28 | { |
---|
29 | friend class Ticker; |
---|
30 | |
---|
31 | public: |
---|
32 | TickerData() : |
---|
33 | todolist(0), autolist(0), |
---|
34 | nentities(0), |
---|
35 | frame(0), recording(0), deltatime(0), bias(0), fps(0), |
---|
36 | quit(0), quitframe(0), quitdelay(20), panic(0) |
---|
37 | { |
---|
38 | for (int i = 0; i < Entity::ALLGROUP_END; i++) |
---|
39 | list[i] = NULL; |
---|
40 | } |
---|
41 | |
---|
42 | ~TickerData() |
---|
43 | { |
---|
44 | #if !LOL_RELEASE |
---|
45 | if (nentities) |
---|
46 | Log::Error("still %i entities in ticker\n", nentities); |
---|
47 | if (autolist) |
---|
48 | { |
---|
49 | int count = 0; |
---|
50 | for (Entity *e = autolist; e; e = e->m_autonext, count++) |
---|
51 | ; |
---|
52 | Log::Error("still %i autoreleased entities\n", count); |
---|
53 | } |
---|
54 | Log::Debug("%i frames required to quit\n", |
---|
55 | frame - quitframe); |
---|
56 | #endif |
---|
57 | gametick.Push(0); |
---|
58 | delete gamethread; |
---|
59 | delete diskthread; |
---|
60 | } |
---|
61 | |
---|
62 | private: |
---|
63 | /* Entity management */ |
---|
64 | Entity *todolist, *autolist; |
---|
65 | Entity *list[Entity::ALLGROUP_END]; |
---|
66 | int nentities; |
---|
67 | |
---|
68 | /* Fixed framerate management */ |
---|
69 | int frame, recording; |
---|
70 | Timer timer; |
---|
71 | float deltatime, bias, fps; |
---|
72 | |
---|
73 | /* Background threads */ |
---|
74 | static void *GameThreadMain(void *p); |
---|
75 | static void *DrawThreadMain(void *p); /* unused */ |
---|
76 | static void *DiskThreadMain(void *p); |
---|
77 | Thread *gamethread, *drawthread, *diskthread; |
---|
78 | Queue<int> gametick, drawtick; |
---|
79 | |
---|
80 | /* Shutdown management */ |
---|
81 | int quit, quitframe, quitdelay, panic; |
---|
82 | } |
---|
83 | tickerdata; |
---|
84 | |
---|
85 | static TickerData * const data = &tickerdata; |
---|
86 | |
---|
87 | /* |
---|
88 | * Ticker public class |
---|
89 | */ |
---|
90 | |
---|
91 | void Ticker::Register(Entity *entity) |
---|
92 | { |
---|
93 | /* If we are called from its constructor, the object's vtable is not |
---|
94 | * ready yet, so we do not know which group this entity belongs to. Wait |
---|
95 | * until the first tick. */ |
---|
96 | entity->m_gamenext = data->todolist; |
---|
97 | data->todolist = entity; |
---|
98 | /* Objects are autoreleased by default. Put them in a circular list. */ |
---|
99 | entity->m_autorelease = 1; |
---|
100 | entity->m_autonext = data->autolist; |
---|
101 | data->autolist = entity; |
---|
102 | entity->m_ref = 1; |
---|
103 | |
---|
104 | data->nentities++; |
---|
105 | } |
---|
106 | |
---|
107 | void Ticker::Ref(Entity *entity) |
---|
108 | { |
---|
109 | #if !LOL_RELEASE |
---|
110 | if (!entity) |
---|
111 | { |
---|
112 | Log::Error("referencing NULL entity\n"); |
---|
113 | return; |
---|
114 | } |
---|
115 | if (entity->m_destroy) |
---|
116 | Log::Error("referencing entity scheduled for destruction\n"); |
---|
117 | #endif |
---|
118 | if (entity->m_autorelease) |
---|
119 | { |
---|
120 | /* Get the entity out of the m_autorelease list. This is usually |
---|
121 | * very fast since the first entry in autolist is the last |
---|
122 | * registered entity. */ |
---|
123 | for (Entity *e = data->autolist, *prev = NULL; e; |
---|
124 | prev = e, e = e->m_autonext) |
---|
125 | { |
---|
126 | if (e == entity) |
---|
127 | { |
---|
128 | (prev ? prev->m_autonext : data->autolist) = e->m_autonext; |
---|
129 | break; |
---|
130 | } |
---|
131 | } |
---|
132 | entity->m_autorelease = 0; |
---|
133 | } |
---|
134 | else |
---|
135 | entity->m_ref++; |
---|
136 | } |
---|
137 | |
---|
138 | int Ticker::Unref(Entity *entity) |
---|
139 | { |
---|
140 | #if !LOL_RELEASE |
---|
141 | if (!entity) |
---|
142 | { |
---|
143 | Log::Error("dereferencing NULL entity\n"); |
---|
144 | return 0; |
---|
145 | } |
---|
146 | if (entity->m_ref <= 0) |
---|
147 | Log::Error("dereferencing unreferenced entity\n"); |
---|
148 | if (entity->m_autorelease) |
---|
149 | Log::Error("dereferencing autoreleased entity\n"); |
---|
150 | #endif |
---|
151 | return --entity->m_ref; |
---|
152 | } |
---|
153 | |
---|
154 | void *TickerData::GameThreadMain(void *p) |
---|
155 | { |
---|
156 | for (;;) |
---|
157 | { |
---|
158 | int tick = data->gametick.Pop(); |
---|
159 | if (!tick) |
---|
160 | break; |
---|
161 | |
---|
162 | Profiler::Stop(Profiler::STAT_TICK_FRAME); |
---|
163 | Profiler::Start(Profiler::STAT_TICK_FRAME); |
---|
164 | |
---|
165 | Profiler::Start(Profiler::STAT_TICK_GAME); |
---|
166 | |
---|
167 | #if 0 |
---|
168 | Log::Debug("-------------------------------------\n"); |
---|
169 | for (int i = 0; i < Entity::ALLGROUP_END; i++) |
---|
170 | { |
---|
171 | Log::Debug("%s Group %i\n", |
---|
172 | (i < Entity::GAMEGROUP_END) ? "Game" : "Draw", i); |
---|
173 | |
---|
174 | for (Entity *e = data->list[i]; e; ) |
---|
175 | { |
---|
176 | Log::Debug(" \\-- %s (m_ref %i, destroy %i)\n", e->GetName(), e->m_ref, e->m_destroy); |
---|
177 | e = (i < Entity::GAMEGROUP_END) ? e->m_gamenext : e->m_drawnext; |
---|
178 | } |
---|
179 | } |
---|
180 | #endif |
---|
181 | |
---|
182 | data->frame++; |
---|
183 | |
---|
184 | /* If recording with fixed framerate, set deltatime to a fixed value */ |
---|
185 | if (data->recording && data->fps) |
---|
186 | { |
---|
187 | data->deltatime = 1.f / data->fps; |
---|
188 | } |
---|
189 | else |
---|
190 | { |
---|
191 | data->deltatime = data->timer.Get(); |
---|
192 | data->bias += data->deltatime; |
---|
193 | } |
---|
194 | |
---|
195 | /* If shutdown is stuck, kick the first entity we meet and see |
---|
196 | * whether it makes things better. Note that it is always a bug to |
---|
197 | * have referenced entities after 20 frames, but at least this |
---|
198 | * safeguard makes it possible to exit the program cleanly. */ |
---|
199 | if (data->quit && !((data->frame - data->quitframe) % data->quitdelay)) |
---|
200 | { |
---|
201 | int n = 0; |
---|
202 | data->panic = 2 * (data->panic + 1); |
---|
203 | |
---|
204 | for (int i = 0; i < Entity::ALLGROUP_END && n < data->panic; i++) |
---|
205 | for (Entity *e = data->list[i]; e && n < data->panic; e = e->m_gamenext) |
---|
206 | if (e->m_ref) |
---|
207 | { |
---|
208 | #if !LOL_RELEASE |
---|
209 | Log::Error("poking %s\n", e->GetName()); |
---|
210 | #endif |
---|
211 | e->m_ref--; |
---|
212 | n++; |
---|
213 | } |
---|
214 | |
---|
215 | #if !LOL_RELEASE |
---|
216 | if (n) |
---|
217 | Log::Error("%i entities stuck after %i frames, poked %i\n", |
---|
218 | data->nentities, data->quitdelay, n); |
---|
219 | #endif |
---|
220 | |
---|
221 | data->quitdelay = data->quitdelay > 1 ? data->quitdelay / 2 : 1; |
---|
222 | } |
---|
223 | |
---|
224 | /* Garbage collect objects that can be destroyed. We can do this |
---|
225 | * before inserting awaiting objects, because only objects already |
---|
226 | * inthe tick lists can be marked for destruction. */ |
---|
227 | for (int i = 0; i < Entity::ALLGROUP_END; i++) |
---|
228 | for (Entity *e = data->list[i], *prev = NULL; e; ) |
---|
229 | { |
---|
230 | if (e->m_destroy && i < Entity::GAMEGROUP_END) |
---|
231 | { |
---|
232 | /* If entity is to be destroyed, remove it from the |
---|
233 | * game tick list. */ |
---|
234 | (prev ? prev->m_gamenext : data->list[i]) = e->m_gamenext; |
---|
235 | |
---|
236 | e = e->m_gamenext; |
---|
237 | } |
---|
238 | else if (e->m_destroy) |
---|
239 | { |
---|
240 | /* If entity is to be destroyed, remove it from the |
---|
241 | * draw tick list and destroy it. */ |
---|
242 | (prev ? prev->m_drawnext : data->list[i]) = e->m_drawnext; |
---|
243 | |
---|
244 | Entity *tmp = e; |
---|
245 | e = e->m_drawnext; /* Can only be in a draw group list */ |
---|
246 | delete tmp; |
---|
247 | |
---|
248 | data->nentities--; |
---|
249 | } |
---|
250 | else |
---|
251 | { |
---|
252 | if (e->m_ref <= 0 && i >= Entity::DRAWGROUP_BEGIN) |
---|
253 | e->m_destroy = 1; |
---|
254 | prev = e; |
---|
255 | e = (i < Entity::GAMEGROUP_END) ? e->m_gamenext : e->m_drawnext; |
---|
256 | } |
---|
257 | } |
---|
258 | |
---|
259 | /* Insert waiting objects into the appropriate lists */ |
---|
260 | while (data->todolist) |
---|
261 | { |
---|
262 | Entity *e = data->todolist; |
---|
263 | data->todolist = e->m_gamenext; |
---|
264 | |
---|
265 | e->m_gamenext = data->list[e->m_gamegroup]; |
---|
266 | data->list[e->m_gamegroup] = e; |
---|
267 | e->m_drawnext = data->list[e->m_drawgroup]; |
---|
268 | data->list[e->m_drawgroup] = e; |
---|
269 | } |
---|
270 | |
---|
271 | /* Tick objects for the game loop */ |
---|
272 | for (int i = Entity::GAMEGROUP_BEGIN; i < Entity::GAMEGROUP_END; i++) |
---|
273 | for (Entity *e = data->list[i]; e; e = e->m_gamenext) |
---|
274 | if (!e->m_destroy) |
---|
275 | { |
---|
276 | #if !LOL_RELEASE |
---|
277 | if (e->m_tickstate != Entity::STATE_IDLE) |
---|
278 | Log::Error("entity not idle for game tick\n"); |
---|
279 | e->m_tickstate = Entity::STATE_PRETICK_GAME; |
---|
280 | #endif |
---|
281 | e->TickGame(data->deltatime); |
---|
282 | #if !LOL_RELEASE |
---|
283 | if (e->m_tickstate != Entity::STATE_POSTTICK_GAME) |
---|
284 | Log::Error("entity missed super game tick\n"); |
---|
285 | e->m_tickstate = Entity::STATE_IDLE; |
---|
286 | #endif |
---|
287 | } |
---|
288 | |
---|
289 | Profiler::Stop(Profiler::STAT_TICK_GAME); |
---|
290 | |
---|
291 | data->drawtick.Push(1); |
---|
292 | } |
---|
293 | |
---|
294 | data->drawtick.Push(0); |
---|
295 | |
---|
296 | return NULL; |
---|
297 | } |
---|
298 | |
---|
299 | void *TickerData::DrawThreadMain(void *p) |
---|
300 | { |
---|
301 | for (;;) |
---|
302 | { |
---|
303 | int tick = data->drawtick.Pop(); |
---|
304 | if (!tick) |
---|
305 | break; |
---|
306 | |
---|
307 | data->gametick.Push(1); |
---|
308 | } |
---|
309 | |
---|
310 | return NULL; |
---|
311 | } |
---|
312 | |
---|
313 | void *TickerData::DiskThreadMain(void *p) |
---|
314 | { |
---|
315 | return NULL; |
---|
316 | } |
---|
317 | |
---|
318 | void Ticker::SetState(Entity *entity, uint32_t state) |
---|
319 | { |
---|
320 | |
---|
321 | } |
---|
322 | |
---|
323 | void Ticker::SetStateWhenMatch(Entity *entity, uint32_t state, |
---|
324 | Entity *other_entity, uint32_t other_state) |
---|
325 | { |
---|
326 | |
---|
327 | } |
---|
328 | |
---|
329 | void Ticker::Setup(float fps) |
---|
330 | { |
---|
331 | data->fps = fps; |
---|
332 | |
---|
333 | data->gamethread = new Thread(TickerData::GameThreadMain, NULL); |
---|
334 | data->gametick.Push(1); |
---|
335 | |
---|
336 | data->diskthread = new Thread(TickerData::DiskThreadMain, NULL); |
---|
337 | } |
---|
338 | |
---|
339 | void Ticker::TickDraw() |
---|
340 | { |
---|
341 | data->drawtick.Pop(); |
---|
342 | |
---|
343 | Profiler::Start(Profiler::STAT_TICK_DRAW); |
---|
344 | |
---|
345 | Video::Clear(); |
---|
346 | |
---|
347 | Scene::GetDefault(); |
---|
348 | |
---|
349 | /* Tick objects for the draw loop */ |
---|
350 | for (int i = Entity::DRAWGROUP_BEGIN; i < Entity::DRAWGROUP_END; i++) |
---|
351 | { |
---|
352 | switch (i) |
---|
353 | { |
---|
354 | case Entity::DRAWGROUP_HUD: |
---|
355 | Scene::GetDefault()->Render(); |
---|
356 | Video::SetDepth(false); |
---|
357 | break; |
---|
358 | default: |
---|
359 | Video::SetDepth(true); |
---|
360 | break; |
---|
361 | } |
---|
362 | |
---|
363 | for (Entity *e = data->list[i]; e; e = e->m_drawnext) |
---|
364 | if (!e->m_destroy) |
---|
365 | { |
---|
366 | #if !LOL_RELEASE |
---|
367 | if (e->m_tickstate != Entity::STATE_IDLE) |
---|
368 | Log::Error("entity not idle for draw tick\n"); |
---|
369 | e->m_tickstate = Entity::STATE_PRETICK_DRAW; |
---|
370 | #endif |
---|
371 | e->TickDraw(data->deltatime); |
---|
372 | #if !LOL_RELEASE |
---|
373 | if (e->m_tickstate != Entity::STATE_POSTTICK_DRAW) |
---|
374 | Log::Error("entity missed super draw tick\n"); |
---|
375 | e->m_tickstate = Entity::STATE_IDLE; |
---|
376 | #endif |
---|
377 | } |
---|
378 | } |
---|
379 | |
---|
380 | Scene::GetDefault()->Render(); |
---|
381 | Scene::Reset(); |
---|
382 | |
---|
383 | Profiler::Stop(Profiler::STAT_TICK_DRAW); |
---|
384 | Profiler::Start(Profiler::STAT_TICK_BLIT); |
---|
385 | |
---|
386 | /* Signal game thread that it can carry on */ |
---|
387 | data->gametick.Push(1); |
---|
388 | |
---|
389 | /* Clamp FPS */ |
---|
390 | Profiler::Stop(Profiler::STAT_TICK_BLIT); |
---|
391 | |
---|
392 | /* If framerate is fixed, force wait time to 1/FPS. Otherwise, set wait |
---|
393 | * time to 0. */ |
---|
394 | float frametime = data->fps ? 1.f / data->fps : 0.f; |
---|
395 | |
---|
396 | if (frametime > data->bias + .2f) |
---|
397 | frametime = data->bias + .2f; // Don't go below 5 fps |
---|
398 | if (frametime > data->bias) |
---|
399 | data->timer.Wait(frametime - data->bias); |
---|
400 | |
---|
401 | /* If recording, do not try to compensate for lag. */ |
---|
402 | if (!data->recording) |
---|
403 | data->bias -= frametime; |
---|
404 | } |
---|
405 | |
---|
406 | void Ticker::StartRecording() |
---|
407 | { |
---|
408 | data->recording++; |
---|
409 | } |
---|
410 | |
---|
411 | void Ticker::StopRecording() |
---|
412 | { |
---|
413 | data->recording--; |
---|
414 | } |
---|
415 | |
---|
416 | int Ticker::GetFrameNum() |
---|
417 | { |
---|
418 | return data->frame; |
---|
419 | } |
---|
420 | |
---|
421 | void Ticker::Shutdown() |
---|
422 | { |
---|
423 | /* We're bailing out. Release all m_autorelease objects. */ |
---|
424 | while (data->autolist) |
---|
425 | { |
---|
426 | data->autolist->m_ref--; |
---|
427 | data->autolist = data->autolist->m_autonext; |
---|
428 | } |
---|
429 | |
---|
430 | data->quit = 1; |
---|
431 | data->quitframe = data->frame; |
---|
432 | } |
---|
433 | |
---|
434 | int Ticker::Finished() |
---|
435 | { |
---|
436 | return !data->nentities; |
---|
437 | } |
---|
438 | |
---|
439 | } /* namespace lol */ |
---|
440 | |
---|