source: trunk/src/ticker.cpp @ 619

Last change on this file since 619 was 619, checked in by sam, 11 years ago

Add a recording mode to Ticker that ensures fixed deltatime even when
lagging behind.

  • Property svn:keywords set to Id
File size: 10.1 KB
Line 
1//
2// Lol Engine
3//
4// Copyright: (c) 2010-2011 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 <cstdio>
17#include <stdint.h>
18
19#include "core.h"
20
21/*
22 * Ticker implementation class
23 */
24
25static class TickerData
26{
27    friend class Ticker;
28
29public:
30    TickerData() :
31        todolist(0), autolist(0),
32        nentities(0),
33        frame(0), benchmark(0), recording(0), deltams(0), bias(0), fps(0),
34        quit(0), quitframe(0), quitdelay(20), panic(0)
35    {
36        for (int i = 0; i < Entity::ALLGROUP_END; i++)
37            list[i] = NULL;
38    }
39
40    ~TickerData()
41    {
42#if !FINAL_RELEASE
43        if (nentities)
44            fprintf(stderr, "ERROR: still %i entities in ticker\n", nentities);
45        if (autolist)
46        {
47            int count = 0;
48            for (Entity *e = autolist; e; e = e->autonext, count++)
49                ;
50            fprintf(stderr, "ERROR: still %i autoreleased entities\n", count);
51        }
52        fprintf(stderr, "INFO: %i frames required to quit\n",
53                frame - quitframe);
54#endif
55    }
56
57private:
58    /* Entity management */
59    Entity *todolist, *autolist;
60    Entity *list[Entity::ALLGROUP_END];
61    int nentities;
62
63    /* Fixed framerate management */
64    int frame, benchmark, recording;
65    Timer timer;
66    float deltams, bias, fps;
67
68    /* Shutdown management */
69    int quit, quitframe, quitdelay, panic;
70}
71tickerdata;
72
73static TickerData * const data = &tickerdata;
74
75/*
76 * Ticker public class
77 */
78
79void Ticker::Register(Entity *entity)
80{
81    /* If we are called from its constructor, the object's vtable is not
82     * ready yet, so we do not know which group this entity belongs to. Wait
83     * until the first tick. */
84    entity->gamenext = data->todolist;
85    data->todolist = entity;
86    /* Objects are autoreleased by default. Put them in a circular list. */
87    entity->autorelease = 1;
88    entity->autonext = data->autolist;
89    data->autolist = entity;
90    entity->ref = 1;
91
92    data->nentities++;
93}
94
95void Ticker::Ref(Entity *entity)
96{
97#if !FINAL_RELEASE
98    if (!entity)
99    {
100        fprintf(stderr, "ERROR: refing NULL entity\n");
101        return;
102    }
103    if (entity->destroy)
104        fprintf(stderr, "ERROR: refing entity scheduled for destruction\n");
105#endif
106    if (entity->autorelease)
107    {
108        /* Get the entity out of the autorelease list. This is usually
109         * very fast since the first entry in autolist is the last
110         * registered entity. */
111        for (Entity *e = data->autolist, *prev = NULL; e;
112             prev = e, e = e->autonext)
113        {
114            if (e == entity)
115            {
116                (prev ? prev->autonext : data->autolist) = e->autonext;
117                break;
118            }
119        }
120        entity->autorelease = 0;
121    }
122    else
123        entity->ref++;
124}
125
126int Ticker::Unref(Entity *entity)
127{
128#if !FINAL_RELEASE
129    if (!entity)
130    {
131        fprintf(stderr, "ERROR: dereferencing NULL entity\n");
132        return 0;
133    }
134    if (entity->ref <= 0)
135        fprintf(stderr, "ERROR: dereferencing unreferenced entity\n");
136    if (entity->autorelease)
137        fprintf(stderr, "ERROR: dereferencing autoreleased entity\n");
138#endif
139    return --entity->ref;
140}
141
142void Ticker::Setup(float fps)
143{
144    data->fps = fps;
145}
146
147void Ticker::TickGame()
148{
149    Profiler::Stop(Profiler::STAT_TICK_FRAME);
150    Profiler::Start(Profiler::STAT_TICK_FRAME);
151
152    Profiler::Start(Profiler::STAT_TICK_GAME);
153
154#if 0
155    fprintf(stderr, "-------------------------------------\n");
156    for (int i = 0; i < Entity::ALLGROUP_END; i++)
157    {
158        fprintf(stderr, "%s Group %i\n",
159                (i < Entity::GAMEGROUP_END) ? "Game" : "Draw", i);
160
161        for (Entity *e = data->list[i]; e; )
162        {
163            fprintf(stderr, "  \\-- %s (ref %i, destroy %i)\n", e->GetName(), e->ref, e->destroy);
164            e = (i < Entity::GAMEGROUP_END) ? e->gamenext : e->drawnext;
165        }
166    }
167#endif
168
169    data->frame++;
170
171    if (data->recording && data->fps)
172    {
173        data->deltams = 1000.0f / data->fps;
174    }
175    else
176    {
177        data->deltams = data->timer.GetMs();
178        data->bias += data->deltams;
179    }
180
181    /* If shutdown is stuck, kick the first entity we meet and see
182     * whether it makes things better. Note that it is always a bug to
183     * have referenced entities after 20 frames, but at least this
184     * safeguard makes it possible to exit the program cleanly. */
185    if (data->quit && !((data->frame - data->quitframe) % data->quitdelay))
186    {
187        int n = 0;
188        data->panic = 2 * (data->panic + 1);
189
190        for (int i = 0; i < Entity::ALLGROUP_END && n < data->panic; i++)
191        for (Entity *e = data->list[i]; e && n < data->panic; e = e->gamenext)
192            if (e->ref)
193            {
194#if !FINAL_RELEASE
195                fprintf(stderr, "ERROR: poking %s\n", e->GetName());
196#endif
197                e->ref--;
198                n++;
199            }
200
201#if !FINAL_RELEASE
202        if (n)
203            fprintf(stderr, "ERROR: %i entities stuck after %i frames, "
204                    "poked %i\n", data->nentities, data->quitdelay, n);
205#endif
206
207        data->quitdelay = data->quitdelay > 1 ? data->quitdelay / 2 : 1;
208    }
209
210    /* Garbage collect objects that can be destroyed. We can do this
211     * before inserting awaiting objects, because only objects already in
212     * the tick lists can be marked for destruction. */
213    for (int i = 0; i < Entity::ALLGROUP_END; i++)
214        for (Entity *e = data->list[i], *prev = NULL; e; )
215        {
216            if (e->destroy && i < Entity::GAMEGROUP_END)
217            {
218                /* If entity is to be destroyed, remove it from the
219                 * game tick list. */
220                (prev ? prev->gamenext : data->list[i]) = e->gamenext;
221
222                e = e->gamenext;
223            }
224            else if (e->destroy)
225            {
226                /* If entity is to be destroyed, remove it from the
227                 * draw tick list and destroy it. */
228                (prev ? prev->drawnext : data->list[i]) = e->drawnext;
229
230                Entity *tmp = e;
231                e = e->drawnext; /* Can only be in a draw group list */
232                delete tmp;
233
234                data->nentities--;
235            }
236            else
237            {
238                if (e->ref <= 0 && i >= Entity::DRAWGROUP_BEGIN)
239                    e->destroy = 1;
240                prev = e;
241                e = (i < Entity::GAMEGROUP_END) ? e->gamenext : e->drawnext;
242            }
243        }
244
245    /* Insert waiting objects into the appropriate lists */
246    while (data->todolist)
247    {
248        Entity *e = data->todolist;
249        data->todolist = e->gamenext;
250
251        e->gamenext = data->list[e->gamegroup];
252        data->list[e->gamegroup] = e;
253        e->drawnext = data->list[e->drawgroup];
254        data->list[e->drawgroup] = e;
255    }
256
257    /* Tick objects for the game loop */
258    for (int i = Entity::GAMEGROUP_BEGIN; i < Entity::GAMEGROUP_END; i++)
259        for (Entity *e = data->list[i]; e; e = e->gamenext)
260            if (!e->destroy)
261            {
262#if !FINAL_RELEASE
263                if (e->state != Entity::STATE_IDLE)
264                    fprintf(stderr, "ERROR: entity not idle for game tick\n");
265                e->state = Entity::STATE_PRETICK_GAME;
266#endif
267                e->TickGame(data->deltams);
268#if !FINAL_RELEASE
269                if (e->state != Entity::STATE_POSTTICK_GAME)
270                    fprintf(stderr, "ERROR: entity missed super game tick\n");
271                e->state = Entity::STATE_IDLE;
272#endif
273            }
274
275    Profiler::Stop(Profiler::STAT_TICK_GAME);
276}
277
278void Ticker::TickDraw()
279{
280    Profiler::Start(Profiler::STAT_TICK_DRAW);
281
282    Video::Clear();
283
284    Scene::GetDefault();
285
286    /* Tick objects for the draw loop */
287    for (int i = Entity::DRAWGROUP_BEGIN; i < Entity::DRAWGROUP_END; i++)
288    {
289        switch (i)
290        {
291        case Entity::DRAWGROUP_HUD:
292            Scene::GetDefault()->Render();
293            Video::SetDepth(false);
294            break;
295        default:
296            Video::SetDepth(true);
297            break;
298        }
299
300        for (Entity *e = data->list[i]; e; e = e->drawnext)
301            if (!e->destroy)
302            {
303#if !FINAL_RELEASE
304                if (e->state != Entity::STATE_IDLE)
305                    fprintf(stderr, "ERROR: entity not idle for draw tick\n");
306                e->state = Entity::STATE_PRETICK_DRAW;
307#endif
308                e->TickDraw(data->deltams);
309#if !FINAL_RELEASE
310                if (e->state != Entity::STATE_POSTTICK_DRAW)
311                    fprintf(stderr, "ERROR: entity missed super draw tick\n");
312                e->state = Entity::STATE_IDLE;
313#endif
314            }
315    }
316
317    Scene::Reset();
318
319    Profiler::Stop(Profiler::STAT_TICK_DRAW);
320    Profiler::Start(Profiler::STAT_TICK_BLIT);
321}
322
323void Ticker::ClampFps()
324{
325    Profiler::Stop(Profiler::STAT_TICK_BLIT);
326
327    /* If benchmarking, set wait time to 0. If FPS are fixed, force wait
328     * time to 1/FPS. Otherwise, set wait time to 0. */
329    float framems = (!data->benchmark && data->fps) ? 1000.0f / data->fps
330                                                    : 0.0f;
331
332    if (!data->benchmark)
333    {
334        if (framems > data->bias + 200.0f)
335            framems = data->bias + 200.0f; // Don't go below 5 fps
336        if (framems > data->bias)
337            data->timer.WaitMs(framems - data->bias);
338    }
339
340    if (!data->recording)
341        data->bias -= framems;
342}
343
344void Ticker::StartBenchmark()
345{
346    data->benchmark++;
347}
348
349void Ticker::StopBenchmark()
350{
351    data->benchmark--;
352}
353
354void Ticker::StartRecording()
355{
356    data->recording++;
357}
358
359void Ticker::StopRecording()
360{
361    data->recording--;
362}
363
364int Ticker::GetFrameNum()
365{
366    return data->frame;
367}
368
369void Ticker::Shutdown()
370{
371    /* We're bailing out. Release all autorelease objects. */
372    while (data->autolist)
373    {
374        data->autolist->ref--;
375        data->autolist = data->autolist->autonext;
376    }
377
378    data->quit = 1;
379    data->quitframe = data->frame;
380}
381
382int Ticker::Finished()
383{
384    return !data->nentities;
385}
386
Note: See TracBrowser for help on using the repository browser.