2025-07-23 10:19:42 -07:00
/* Copyright (C) 2025 Wildfire Games.
2023-12-02 16:30:12 -08:00
* This file is part of 0 A . D .
2009-04-18 10:00:33 -07:00
*
2023-12-02 16:30:12 -08:00
* 0 A . D . is free software : you can redistribute it and / or modify
2009-04-18 10:00:33 -07:00
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation , either version 2 of the License , or
* ( at your option ) any later version .
*
2023-12-02 16:30:12 -08:00
* 0 A . D . is distributed in the hope that it will be useful ,
2009-04-18 10:00:33 -07:00
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License
2023-12-02 16:30:12 -08:00
* along with 0 A . D . If not , see < http : //www.gnu.org/licenses/>.
2009-04-18 10:00:33 -07:00
*/
2006-04-23 16:14:18 -07:00
// FIFO queue of load 'functors' with time limit; enables displaying
// load progress without resorting to threads (complicated).
# include "precompiled.h"
2025-07-23 10:19:42 -07:00
# include "Loader.h"
2006-04-23 16:14:18 -07:00
2025-07-23 10:19:42 -07:00
# include "lib/code_annotation.h"
# include "lib/secure_crt.h"
# include "lib/utf8.h"
2006-04-23 16:14:18 -07:00
2025-07-23 10:19:42 -07:00
# include <deque>
# include <numeric>
2025-10-20 12:55:24 -07:00
# include <optional>
2025-07-23 10:19:42 -07:00
# include <string>
# include <utility>
2006-04-23 16:14:18 -07:00
2025-10-20 12:35:40 -07:00
namespace PS : : Loader
{
namespace
{
2025-12-16 10:35:25 -08:00
// set by PS::Loader::EndRegistering; may be 0 during development when
2006-04-23 16:14:18 -07:00
// estimated task durations haven't yet been set.
2025-10-20 12:35:40 -07:00
double total_estimated_duration ;
2006-04-23 16:14:18 -07:00
2025-12-16 10:35:25 -08:00
// total time spent loading so far, set by PS::Loader::ProgressiveLoad.
2006-04-23 16:14:18 -07:00
// we need a persistent counter so it can be reset after each load.
// this also accumulates less errors than:
// progress += task_estimated / total_estimated.
2025-10-20 12:35:40 -07:00
double estimated_duration_tally ;
2006-04-23 16:14:18 -07:00
// needed for report of how long each individual task took.
2025-10-20 12:35:40 -07:00
double task_elapsed_time ;
2006-04-23 16:14:18 -07:00
// main purpose is to indicate whether a load is in progress, so that
2025-12-16 10:35:25 -08:00
// PS::Loader::ProgressiveLoad can return 0 iff loading just completed.
2006-04-23 16:14:18 -07:00
// the REGISTERING state allows us to detect 2 simultaneous loads (bogus);
2025-12-16 10:35:25 -08:00
// FIRST_LOAD is used to skip the first timeslice (see PS::Loader::ProgressiveLoad).
2025-10-20 12:35:40 -07:00
enum
2006-04-23 16:14:18 -07:00
{
IDLE ,
REGISTERING ,
FIRST_LOAD ,
2007-12-20 12:21:45 -08:00
LOADING
2006-04-23 16:14:18 -07:00
}
state = IDLE ;
// holds all state for one load request; stored in queue.
struct LoadRequest
{
2025-12-16 10:35:25 -08:00
// member documentation is in PS::Loader::Register (avoid duplication).
2006-04-23 16:14:18 -07:00
LoadFunc func ;
2019-09-30 01:49:00 -07:00
// Translatable string shown to the player.
2025-10-17 07:19:46 -07:00
std : : wstring description ;
2006-04-23 16:14:18 -07:00
int estimated_duration_ms ;
2025-12-16 10:35:25 -08:00
// PS::Loader::Register gets these as parameters; pack everything together.
2025-10-17 07:19:46 -07:00
LoadRequest ( LoadFunc func_ , std : : wstring desc_ , int ms_ )
: func ( std : : move ( func_ ) ) , description ( std : : move ( desc_ ) ) , estimated_duration_ms ( ms_ )
2006-04-23 16:14:18 -07:00
{
}
} ;
2025-10-20 12:35:40 -07:00
using LoadRequests = std : : deque < LoadRequest > ;
LoadRequests load_requests ;
2025-10-20 12:55:24 -07:00
std : : optional < PS : : Loader : : Task > currentTask ;
2025-10-20 12:35:40 -07:00
} // anonymous namespace
2023-06-26 11:35:34 -07:00
2006-04-23 16:14:18 -07:00
// call before starting to register load requests.
// this routine is provided so we can prevent 2 simultaneous load operations,
// which is bogus. that can happen by clicking the load button quickly,
// or issuing via console while already loading.
2025-10-20 12:35:40 -07:00
void BeginRegistering ( )
2006-04-23 16:14:18 -07:00
{
2011-04-30 06:01:45 -07:00
ENSURE ( state = = IDLE ) ;
2006-04-23 16:14:18 -07:00
state = REGISTERING ;
load_requests . clear ( ) ;
}
// register a task (later processed in FIFO order).
// <func>: function that will perform the actual work; see LoadFunc.
2019-09-30 01:49:00 -07:00
// <param>: (optional) parameter/persistent state.
2006-04-23 16:14:18 -07:00
// <description>: user-visible description of the current task, e.g.
// "Loading Textures".
// <estimated_duration_ms>: used to calculate progress, and when checking
// whether there is enough of the time budget left to process this task
// (reduces timeslice overruns, making the main loop more responsive).
2025-10-20 12:35:40 -07:00
void Register ( LoadFunc func , std : : wstring description , int estimatedDurationMs )
2006-04-23 16:14:18 -07:00
{
2025-12-16 10:35:25 -08:00
ENSURE ( state = = REGISTERING ) ; // must be called between PS::Loader::(Begin|End)Register
2006-04-23 16:14:18 -07:00
2025-10-20 12:55:24 -07:00
load_requests . push_back ( { std : : move ( func ) , std : : move ( description ) , estimatedDurationMs } ) ;
2006-04-23 16:14:18 -07:00
}
// call when finished registering tasks; subsequent calls to
2025-12-16 10:35:25 -08:00
// PS::Loader::ProgressiveLoad will then work off the queued entries.
2025-10-20 12:35:40 -07:00
void EndRegistering ( )
2006-04-23 16:14:18 -07:00
{
2011-04-30 06:01:45 -07:00
ENSURE ( state = = REGISTERING ) ;
ENSURE ( ! load_requests . empty ( ) ) ;
2006-04-23 16:14:18 -07:00
state = FIRST_LOAD ;
estimated_duration_tally = 0.0 ;
task_elapsed_time = 0.0 ;
2020-11-30 07:45:05 -08:00
total_estimated_duration = std : : accumulate ( load_requests . begin ( ) , load_requests . end ( ) , 0.0 ,
[ ] ( double partial_result , const LoadRequest & lr ) - > double { return partial_result + lr . estimated_duration_ms * 1e-3 ; } ) ;
2006-04-23 16:14:18 -07:00
}
// immediately cancel this load; no further tasks will be processed.
// used to abort loading upon user request or failure.
2025-12-16 10:35:25 -08:00
// note: no special notification will be returned by PS::Loader::ProgressiveLoad.
2025-10-20 12:35:40 -07:00
void Cancel ( )
2006-04-23 16:14:18 -07:00
{
// the queue doesn't need to be emptied now; that'll happen during the
2025-12-16 10:35:25 -08:00
// next PS::Loader::StartRegistering. for now, it is sufficient to set the
// state, so that PS::Loader::ProgressiveLoad is a no-op.
2008-07-12 03:45:11 -07:00
state = IDLE ;
2006-04-23 16:14:18 -07:00
}
2025-10-20 12:35:40 -07:00
namespace
{
2025-12-16 10:35:25 -08:00
// helper routine for PS::Loader::ProgressiveLoad.
2006-04-23 16:14:18 -07:00
// tries to prevent starting a long task when at the end of a timeslice.
2025-10-20 12:35:40 -07:00
bool HaveTimeForNextTask ( double time_left , double time_budget , int estimated_duration_ms )
2006-04-23 16:14:18 -07:00
{
// have already exceeded our time budget
if ( time_left < = 0.0 )
return false ;
// we haven't started a request yet this timeslice. start it even if
// it's longer than time_budget to make sure there is progress.
if ( time_left = = time_budget )
return true ;
// check next task length. we want a lengthy task to happen in its own
// timeslice so that its description is displayed beforehand.
const double estimated_duration = estimated_duration_ms * 1e-3 ;
if ( time_left + estimated_duration > time_budget * 1.20 )
return false ;
return true ;
}
2025-10-20 12:35:40 -07:00
}
2006-04-23 16:14:18 -07:00
2025-10-20 12:35:40 -07:00
ProgressiveLoadResult ProgressiveLoad ( double time_budget )
2006-04-23 16:14:18 -07:00
{
2025-10-20 12:35:40 -07:00
ProgressiveLoadResult ret ;
2006-04-23 16:14:18 -07:00
double progress = 0.0 ; // used to set progress_percent
double time_left = time_budget ;
// don't do any work the first time around so that a graphics update
// happens before the first (probably lengthy) timeslice.
if ( state = = FIRST_LOAD )
{
state = LOADING ;
2025-10-17 07:19:46 -07:00
ret . status = ERR : : TIMED_OUT ; // make caller think we did something
2006-04-23 16:14:18 -07:00
// progress already set to 0.0; that'll be passed back.
goto done ;
}
// we're called unconditionally from the main loop, so this isn't
// an error; there is just nothing to do.
if ( state ! = LOADING )
2025-10-17 07:19:46 -07:00
return { } ;
2006-04-23 16:14:18 -07:00
while ( ! load_requests . empty ( ) )
{
// get next task; abort if there's not enough time left for it.
const LoadRequest & lr = load_requests . front ( ) ;
const double estimated_duration = lr . estimated_duration_ms * 1e-3 ;
if ( ! HaveTimeForNextTask ( time_left , time_budget , lr . estimated_duration_ms ) )
{
2025-10-17 07:19:46 -07:00
ret . status = ERR : : TIMED_OUT ;
2006-04-23 16:14:18 -07:00
goto done ;
}
// call this task's function and bill elapsed time.
2008-01-07 12:03:19 -08:00
const double t0 = timer_Time ( ) ;
2025-10-20 12:55:24 -07:00
if ( ! currentTask . has_value ( ) )
currentTask . emplace ( lr . func ( ) ) ;
try
{
currentTask - > Step ( time_left ) ;
}
catch ( . . . )
{
currentTask . reset ( ) ;
throw ;
}
const bool timed_out = ! currentTask - > IsDone ( ) ;
2008-01-07 12:03:19 -08:00
const double elapsed_time = timer_Time ( ) - t0 ;
2006-04-23 16:14:18 -07:00
time_left - = elapsed_time ;
task_elapsed_time + = elapsed_time ;
// either finished entirely, or failed => remove from queue.
2006-06-29 15:52:50 -07:00
if ( ! timed_out )
2006-04-23 16:14:18 -07:00
{
2015-02-13 17:45:13 -08:00
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 ) ;
2006-04-23 16:14:18 -07:00
task_elapsed_time = 0.0 ;
estimated_duration_tally + = estimated_duration ;
load_requests . pop_front ( ) ;
}
// calculate progress (only possible if estimates have been given)
if ( total_estimated_duration ! = 0.0 )
{
double current_estimate = estimated_duration_tally ;
// function interrupted itself; add its estimated progress.
// note: monotonicity is guaranteed since we never add more than
// its estimated_duration_ms.
2006-06-29 15:52:50 -07:00
if ( timed_out )
2025-10-20 12:55:24 -07:00
current_estimate + = estimated_duration * currentTask - > GetProgress ( ) / 100.0 ;
2006-04-23 16:14:18 -07:00
progress = current_estimate / total_estimated_duration ;
}
// do we need to continue?
// .. function interrupted itself, i.e. timed out; abort.
2006-06-29 15:52:50 -07:00
if ( timed_out )
2006-04-23 16:14:18 -07:00
{
2025-10-17 07:19:46 -07:00
ret . status = ERR : : TIMED_OUT ;
2006-04-23 16:14:18 -07:00
goto done ;
}
2025-12-24 08:13:39 -08:00
const int taskResult { std : : exchange ( currentTask , std : : nullopt ) - > Get ( ) } ;
2006-04-23 16:14:18 -07:00
// .. failed; abort. loading will continue when we're called in
// the next iteration of the main loop.
// rationale: bail immediately instead of remembering the first
2011-04-09 22:31:18 -07:00
// error that came up so we can report all errors that happen.
2025-12-24 08:13:39 -08:00
if ( taskResult < 0 )
{
ret . status = static_cast < Status > ( taskResult ) ;
goto done ;
}
2025-12-16 10:35:25 -08:00
// .. function called PS::Loader::Cancel; abort. return OK since this is an
2011-12-22 06:04:32 -08:00
// intentional cancellation, not an error.
2025-12-24 08:13:39 -08:00
if ( state ! = LOADING )
{
2025-10-17 07:19:46 -07:00
ret . status = INFO : : OK ;
2025-12-24 08:13:39 -08:00
goto done ;
}
2006-04-23 16:14:18 -07:00
// .. succeeded; continue and process next queued task.
}
// queue is empty, we just finished.
state = IDLE ;
2025-10-17 07:19:46 -07:00
ret . status = INFO : : ALL_COMPLETE ;
2006-04-23 16:14:18 -07:00
// set output params (there are several return points above)
done :
2025-10-17 07:19:46 -07:00
ret . progressPercent = static_cast < int > ( progress * 100.0 ) ;
ENSURE ( 0 < = ret . progressPercent & & ret . progressPercent < = 100 ) ;
2006-04-23 16:14:18 -07:00
// we want the next task, instead of what just completed:
// it will be displayed during the next load phase.
if ( ! load_requests . empty ( ) )
2025-10-17 07:19:46 -07:00
ret . nextDescription = load_requests . front ( ) . description ;
2006-04-23 16:14:18 -07:00
2025-10-17 07:19:46 -07:00
debug_printf ( " LOADER| returning; desc=%s progress=%d \n " , utf8_from_wstring ( ret . nextDescription ) . c_str ( ) , ret . progressPercent ) ;
2006-04-23 16:14:18 -07:00
return ret ;
}
// immediately process all queued load requests.
// returns 0 on success or a negative error code.
2025-10-20 12:35:40 -07:00
Status NonprogressiveLoad ( )
2006-04-23 16:14:18 -07:00
{
const double time_budget = 100.0 ;
// large enough so that individual functions won't time out
// (that'd waste time).
for ( ; ; )
{
2025-10-20 12:35:40 -07:00
const auto [ ret , description , progress_percent ] = ProgressiveLoad ( time_budget ) ;
2006-04-23 16:14:18 -07:00
switch ( ret )
{
2006-09-22 06:19:40 -07:00
case INFO : : OK :
2009-11-03 13:46:35 -08:00
debug_warn ( L " No load in progress " ) ;
2006-09-22 06:19:40 -07:00
return INFO : : OK ;
case INFO : : ALL_COMPLETE :
return INFO : : OK ;
case ERR : : TIMED_OUT :
2006-04-23 16:14:18 -07:00
break ; // continue loading
default :
2011-05-03 05:38:42 -07:00
WARN_RETURN_STATUS_IF_ERR ( ret ) ; // failed; complain
2006-04-23 16:14:18 -07:00
}
}
}
2025-10-20 12:35:40 -07:00
} // namespace PS::Loader