Line data Source code
1 : /* Copyright (C) 2019 Wildfire Games.
2 : * This file is part of 0 A.D.
3 : *
4 : * 0 A.D. is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 2 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * 0 A.D. is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
16 : */
17 :
18 : // FIFO queue of load 'functors' with time limit; enables displaying
19 : // load progress without resorting to threads (complicated).
20 :
21 : #include "precompiled.h"
22 :
23 : #include <deque>
24 : #include <numeric>
25 :
26 : #include "lib/timer.h"
27 : #include "CStr.h"
28 : #include "Loader.h"
29 : #include "LoaderThunks.h"
30 :
31 :
32 : // set by LDR_EndRegistering; may be 0 during development when
33 : // estimated task durations haven't yet been set.
34 : static double total_estimated_duration;
35 :
36 : // total time spent loading so far, set by LDR_ProgressiveLoad.
37 : // we need a persistent counter so it can be reset after each load.
38 : // this also accumulates less errors than:
39 : // progress += task_estimated / total_estimated.
40 : static double estimated_duration_tally;
41 :
42 : // needed for report of how long each individual task took.
43 : static double task_elapsed_time;
44 :
45 : // main purpose is to indicate whether a load is in progress, so that
46 : // LDR_ProgressiveLoad can return 0 iff loading just completed.
47 : // the REGISTERING state allows us to detect 2 simultaneous loads (bogus);
48 : // FIRST_LOAD is used to skip the first timeslice (see LDR_ProgressiveLoad).
49 : static enum
50 : {
51 : IDLE,
52 : REGISTERING,
53 : FIRST_LOAD,
54 : LOADING
55 : }
56 : state = IDLE;
57 :
58 :
59 : // holds all state for one load request; stored in queue.
60 0 : struct LoadRequest
61 : {
62 : // member documentation is in LDR_Register (avoid duplication).
63 :
64 : LoadFunc func;
65 :
66 : // MemFun_t<T> instance
67 : std::shared_ptr<void> param;
68 :
69 : // Translatable string shown to the player.
70 : CStrW description;
71 :
72 : int estimated_duration_ms;
73 :
74 : // LDR_Register gets these as parameters; pack everything together.
75 0 : LoadRequest(LoadFunc func_, std::shared_ptr<void> param_, const wchar_t* desc_, int ms_)
76 0 : : func(func_), param(param_), description(desc_),
77 0 : estimated_duration_ms(ms_)
78 : {
79 0 : }
80 : };
81 :
82 : typedef std::deque<LoadRequest> LoadRequests;
83 1 : static LoadRequests load_requests;
84 :
85 : // call before starting to register load requests.
86 : // this routine is provided so we can prevent 2 simultaneous load operations,
87 : // which is bogus. that can happen by clicking the load button quickly,
88 : // or issuing via console while already loading.
89 0 : void LDR_BeginRegistering()
90 : {
91 0 : ENSURE(state == IDLE);
92 :
93 0 : state = REGISTERING;
94 0 : load_requests.clear();
95 0 : }
96 :
97 :
98 : // register a task (later processed in FIFO order).
99 : // <func>: function that will perform the actual work; see LoadFunc.
100 : // <param>: (optional) parameter/persistent state.
101 : // <description>: user-visible description of the current task, e.g.
102 : // "Loading Textures".
103 : // <estimated_duration_ms>: used to calculate progress, and when checking
104 : // whether there is enough of the time budget left to process this task
105 : // (reduces timeslice overruns, making the main loop more responsive).
106 0 : void LDR_Register(LoadFunc func, std::shared_ptr<void> param, const wchar_t* description,
107 : int estimated_duration_ms)
108 : {
109 0 : ENSURE(state == REGISTERING); // must be called between LDR_(Begin|End)Register
110 :
111 0 : const LoadRequest lr(func, param, description, estimated_duration_ms);
112 0 : load_requests.push_back(lr);
113 0 : }
114 :
115 :
116 : // call when finished registering tasks; subsequent calls to
117 : // LDR_ProgressiveLoad will then work off the queued entries.
118 0 : void LDR_EndRegistering()
119 : {
120 0 : ENSURE(state == REGISTERING);
121 0 : ENSURE(!load_requests.empty());
122 :
123 0 : state = FIRST_LOAD;
124 0 : estimated_duration_tally = 0.0;
125 0 : task_elapsed_time = 0.0;
126 0 : total_estimated_duration = std::accumulate(load_requests.begin(), load_requests.end(), 0.0,
127 0 : [](double partial_result, const LoadRequest& lr) -> double { return partial_result + lr.estimated_duration_ms * 1e-3; });
128 0 : }
129 :
130 :
131 : // immediately cancel this load; no further tasks will be processed.
132 : // used to abort loading upon user request or failure.
133 : // note: no special notification will be returned by LDR_ProgressiveLoad.
134 0 : void LDR_Cancel()
135 : {
136 : // the queue doesn't need to be emptied now; that'll happen during the
137 : // next LDR_StartRegistering. for now, it is sufficient to set the
138 : // state, so that LDR_ProgressiveLoad is a no-op.
139 0 : state = IDLE;
140 0 : }
141 :
142 : // helper routine for LDR_ProgressiveLoad.
143 : // tries to prevent starting a long task when at the end of a timeslice.
144 0 : static bool HaveTimeForNextTask(double time_left, double time_budget, int estimated_duration_ms)
145 : {
146 : // have already exceeded our time budget
147 0 : if(time_left <= 0.0)
148 0 : return false;
149 :
150 : // we haven't started a request yet this timeslice. start it even if
151 : // it's longer than time_budget to make sure there is progress.
152 0 : if(time_left == time_budget)
153 0 : return true;
154 :
155 : // check next task length. we want a lengthy task to happen in its own
156 : // timeslice so that its description is displayed beforehand.
157 0 : const double estimated_duration = estimated_duration_ms*1e-3;
158 0 : if(time_left+estimated_duration > time_budget*1.20)
159 0 : return false;
160 :
161 0 : return true;
162 : }
163 :
164 :
165 : // process as many of the queued tasks as possible within <time_budget> [s].
166 : // if a task is lengthy, the budget may be exceeded. call from the main loop.
167 : //
168 : // passes back a description of the next task that will be undertaken
169 : // ("" if finished) and the current progress value.
170 : //
171 : // return semantics:
172 : // - if the final load task just completed, return INFO::ALL_COMPLETE.
173 : // - if loading is in progress but didn't finish, return ERR::TIMED_OUT.
174 : // - if not currently loading (no-op), return 0.
175 : // - any other value indicates a failure; the request has been de-queued.
176 : //
177 : // string interface rationale: for better interoperability, we avoid C++
178 : // std::wstring and PS CStr. since the registered description may not be
179 : // persistent, we can't just store a pointer. returning a pointer to
180 : // our copy of the description doesn't work either, since it's freed when
181 : // the request is de-queued. that leaves writing into caller's buffer.
182 0 : Status LDR_ProgressiveLoad(double time_budget, wchar_t* description, size_t max_chars, int* progress_percent)
183 : {
184 : Status ret; // single exit; this is returned
185 0 : double progress = 0.0; // used to set progress_percent
186 0 : double time_left = time_budget;
187 :
188 : // don't do any work the first time around so that a graphics update
189 : // happens before the first (probably lengthy) timeslice.
190 0 : if(state == FIRST_LOAD)
191 : {
192 0 : state = LOADING;
193 :
194 0 : ret = ERR::TIMED_OUT; // make caller think we did something
195 : // progress already set to 0.0; that'll be passed back.
196 0 : goto done;
197 : }
198 :
199 : // we're called unconditionally from the main loop, so this isn't
200 : // an error; there is just nothing to do.
201 0 : if(state != LOADING)
202 0 : return INFO::OK;
203 :
204 0 : while(!load_requests.empty())
205 : {
206 : // get next task; abort if there's not enough time left for it.
207 0 : const LoadRequest& lr = load_requests.front();
208 0 : const double estimated_duration = lr.estimated_duration_ms*1e-3;
209 0 : if(!HaveTimeForNextTask(time_left, time_budget, lr.estimated_duration_ms))
210 : {
211 0 : ret = ERR::TIMED_OUT;
212 0 : goto done;
213 : }
214 :
215 : // call this task's function and bill elapsed time.
216 0 : const double t0 = timer_Time();
217 0 : int status = lr.func(lr.param, time_left);
218 0 : const bool timed_out = ldr_was_interrupted(status);
219 0 : const double elapsed_time = timer_Time() - t0;
220 0 : time_left -= elapsed_time;
221 0 : task_elapsed_time += elapsed_time;
222 :
223 : // either finished entirely, or failed => remove from queue.
224 0 : if(!timed_out)
225 : {
226 0 : debug_printf("LOADER| completed %s in %g ms; estimate was %g ms\n", utf8_from_wstring(lr.description).c_str(), task_elapsed_time*1e3, estimated_duration*1e3);
227 0 : task_elapsed_time = 0.0;
228 0 : estimated_duration_tally += estimated_duration;
229 0 : load_requests.pop_front();
230 : }
231 :
232 : // calculate progress (only possible if estimates have been given)
233 0 : if(total_estimated_duration != 0.0)
234 : {
235 0 : double current_estimate = estimated_duration_tally;
236 :
237 : // function interrupted itself; add its estimated progress.
238 : // note: monotonicity is guaranteed since we never add more than
239 : // its estimated_duration_ms.
240 0 : if(timed_out)
241 0 : current_estimate += estimated_duration * status/100.0;
242 :
243 0 : progress = current_estimate / total_estimated_duration;
244 : }
245 :
246 : // do we need to continue?
247 : // .. function interrupted itself, i.e. timed out; abort.
248 0 : if(timed_out)
249 : {
250 0 : ret = ERR::TIMED_OUT;
251 0 : goto done;
252 : }
253 : // .. failed; abort. loading will continue when we're called in
254 : // the next iteration of the main loop.
255 : // rationale: bail immediately instead of remembering the first
256 : // error that came up so we can report all errors that happen.
257 0 : else if(status < 0)
258 : {
259 0 : ret = (Status)status;
260 0 : goto done;
261 : }
262 : // .. function called LDR_Cancel; abort. return OK since this is an
263 : // intentional cancellation, not an error.
264 0 : else if(state != LOADING)
265 : {
266 0 : ret = INFO::OK;
267 0 : goto done;
268 : }
269 : // .. succeeded; continue and process next queued task.
270 : }
271 :
272 : // queue is empty, we just finished.
273 0 : state = IDLE;
274 0 : ret = INFO::ALL_COMPLETE;
275 :
276 :
277 : // set output params (there are several return points above)
278 0 : done:
279 0 : *progress_percent = (int)(progress * 100.0);
280 0 : ENSURE(0 <= *progress_percent && *progress_percent <= 100);
281 :
282 : // we want the next task, instead of what just completed:
283 : // it will be displayed during the next load phase.
284 0 : const wchar_t* new_description = L""; // assume finished
285 0 : if(!load_requests.empty())
286 0 : new_description = load_requests.front().description.c_str();
287 0 : wcscpy_s(description, max_chars, new_description);
288 :
289 0 : debug_printf("LOADER| returning; desc=%s progress=%d\n", utf8_from_wstring(description).c_str(), *progress_percent);
290 :
291 0 : return ret;
292 : }
293 :
294 :
295 : // immediately process all queued load requests.
296 : // returns 0 on success or a negative error code.
297 0 : Status LDR_NonprogressiveLoad()
298 : {
299 0 : const double time_budget = 100.0;
300 : // large enough so that individual functions won't time out
301 : // (that'd waste time).
302 : wchar_t description[100];
303 : int progress_percent;
304 :
305 : for(;;)
306 : {
307 0 : Status ret = LDR_ProgressiveLoad(time_budget, description, ARRAY_SIZE(description), &progress_percent);
308 0 : switch(ret)
309 : {
310 0 : case INFO::OK:
311 0 : debug_warn(L"No load in progress");
312 0 : return INFO::OK;
313 0 : case INFO::ALL_COMPLETE:
314 0 : return INFO::OK;
315 0 : case ERR::TIMED_OUT:
316 0 : break; // continue loading
317 0 : default:
318 0 : WARN_RETURN_STATUS_IF_ERR(ret); // failed; complain
319 : }
320 0 : }
321 3 : }
|