1 /* Copyright (C) 2019 Wildfire Games.
2 * This file is part of 0 A.D.
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.
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.
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/>.
18 // FIFO queue of load 'functors' with time limit; enables displaying
19 // load progress without resorting to threads (complicated).
21 #include "precompiled.h"
26 #include "lib/timer.h"
29 #include "LoaderThunks.h"
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
;
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
;
42 // needed for report of how long each individual task took.
43 static double task_elapsed_time
;
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).
59 // holds all state for one load request; stored in queue.
62 // member documentation is in LDR_Register (avoid duplication).
66 // MemFun_t<T> instance
67 std::shared_ptr
<void> param
;
69 // Translatable string shown to the player.
72 int estimated_duration_ms
;
74 // LDR_Register gets these as parameters; pack everything together.
75 LoadRequest(LoadFunc func_
, std::shared_ptr
<void> param_
, const wchar_t* desc_
, int ms_
)
76 : func(func_
), param(param_
), description(desc_
),
77 estimated_duration_ms(ms_
)
82 typedef std::deque
<LoadRequest
> LoadRequests
;
83 static LoadRequests load_requests
;
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 void LDR_BeginRegistering()
91 ENSURE(state
== IDLE
);
94 load_requests
.clear();
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 void LDR_Register(LoadFunc func
, std::shared_ptr
<void> param
, const wchar_t* description
,
107 int estimated_duration_ms
)
109 ENSURE(state
== REGISTERING
); // must be called between LDR_(Begin|End)Register
111 const LoadRequest
lr(func
, param
, description
, estimated_duration_ms
);
112 load_requests
.push_back(lr
);
116 // call when finished registering tasks; subsequent calls to
117 // LDR_ProgressiveLoad will then work off the queued entries.
118 void LDR_EndRegistering()
120 ENSURE(state
== REGISTERING
);
121 ENSURE(!load_requests
.empty());
124 estimated_duration_tally
= 0.0;
125 task_elapsed_time
= 0.0;
126 total_estimated_duration
= std::accumulate(load_requests
.begin(), load_requests
.end(), 0.0,
127 [](double partial_result
, const LoadRequest
& lr
) -> double { return partial_result
+ lr
.estimated_duration_ms
* 1e-3; });
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.
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.
142 // helper routine for LDR_ProgressiveLoad.
143 // tries to prevent starting a long task when at the end of a timeslice.
144 static bool HaveTimeForNextTask(double time_left
, double time_budget
, int estimated_duration_ms
)
146 // have already exceeded our time budget
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 if(time_left
== time_budget
)
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 const double estimated_duration
= estimated_duration_ms
*1e-3;
158 if(time_left
+estimated_duration
> time_budget
*1.20)
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.
168 // passes back a description of the next task that will be undertaken
169 // ("" if finished) and the current progress value.
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.
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 Status
LDR_ProgressiveLoad(double time_budget
, wchar_t* description
, size_t max_chars
, int* progress_percent
)
184 Status ret
; // single exit; this is returned
185 double progress
= 0.0; // used to set progress_percent
186 double time_left
= time_budget
;
188 // don't do any work the first time around so that a graphics update
189 // happens before the first (probably lengthy) timeslice.
190 if(state
== FIRST_LOAD
)
194 ret
= ERR::TIMED_OUT
; // make caller think we did something
195 // progress already set to 0.0; that'll be passed back.
199 // we're called unconditionally from the main loop, so this isn't
200 // an error; there is just nothing to do.
204 while(!load_requests
.empty())
206 // get next task; abort if there's not enough time left for it.
207 const LoadRequest
& lr
= load_requests
.front();
208 const double estimated_duration
= lr
.estimated_duration_ms
*1e-3;
209 if(!HaveTimeForNextTask(time_left
, time_budget
, lr
.estimated_duration_ms
))
211 ret
= ERR::TIMED_OUT
;
215 // call this task's function and bill elapsed time.
216 const double t0
= timer_Time();
217 int status
= lr
.func(lr
.param
, time_left
);
218 const bool timed_out
= ldr_was_interrupted(status
);
219 const double elapsed_time
= timer_Time() - t0
;
220 time_left
-= elapsed_time
;
221 task_elapsed_time
+= elapsed_time
;
223 // either finished entirely, or failed => remove from queue.
226 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 task_elapsed_time
= 0.0;
228 estimated_duration_tally
+= estimated_duration
;
229 load_requests
.pop_front();
232 // calculate progress (only possible if estimates have been given)
233 if(total_estimated_duration
!= 0.0)
235 double current_estimate
= estimated_duration_tally
;
237 // function interrupted itself; add its estimated progress.
238 // note: monotonicity is guaranteed since we never add more than
239 // its estimated_duration_ms.
241 current_estimate
+= estimated_duration
* status
/100.0;
243 progress
= current_estimate
/ total_estimated_duration
;
246 // do we need to continue?
247 // .. function interrupted itself, i.e. timed out; abort.
250 ret
= ERR::TIMED_OUT
;
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.
259 ret
= (Status
)status
;
262 // .. function called LDR_Cancel; abort. return OK since this is an
263 // intentional cancellation, not an error.
264 else if(state
!= LOADING
)
269 // .. succeeded; continue and process next queued task.
272 // queue is empty, we just finished.
274 ret
= INFO::ALL_COMPLETE
;
277 // set output params (there are several return points above)
279 *progress_percent
= (int)(progress
* 100.0);
280 ENSURE(0 <= *progress_percent
&& *progress_percent
<= 100);
282 // we want the next task, instead of what just completed:
283 // it will be displayed during the next load phase.
284 const wchar_t* new_description
= L
""; // assume finished
285 if(!load_requests
.empty())
286 new_description
= load_requests
.front().description
.c_str();
287 wcscpy_s(description
, max_chars
, new_description
);
289 debug_printf("LOADER| returning; desc=%s progress=%d\n", utf8_from_wstring(description
).c_str(), *progress_percent
);
295 // immediately process all queued load requests.
296 // returns 0 on success or a negative error code.
297 Status
LDR_NonprogressiveLoad()
299 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
;
307 Status ret
= LDR_ProgressiveLoad(time_budget
, description
, ARRAY_SIZE(description
), &progress_percent
);
311 debug_warn(L
"No load in progress");
313 case INFO::ALL_COMPLETE
:
316 break; // continue loading
318 WARN_RETURN_STATUS_IF_ERR(ret
); // failed; complain