LNXSDK/Kha/Sources/kha/Scheduler.hx

555 lines
14 KiB
Haxe
Raw Permalink Normal View History

2025-01-22 16:18:30 +01:00
package kha;
class TimeTask {
public var task: Void->Bool;
public var start: Float;
public var period: Float;
public var duration: Float;
public var next: Float;
public var id: Int;
public var groupId: Int;
public var active: Bool;
public var paused: Bool;
public function new() {}
}
class FrameTask {
public var task: Void->Bool;
public var priority: Int;
public var id: Int;
public var active: Bool;
public var paused: Bool;
public function new(task: Void->Bool, priority: Int, id: Int) {
this.task = task;
this.priority = priority;
this.id = id;
active = true;
paused = false;
}
}
class Scheduler {
static var timeTasks: Array<TimeTask>;
static var pausedTimeTasks: Array<TimeTask>;
static var outdatedTimeTasks: Array<TimeTask>;
static var timeTasksScratchpad: Array<TimeTask>;
static inline var timeWarpSaveTime: Float = 10.0;
static var frameTasks: Array<FrameTask>;
static var toDeleteFrame: Array<FrameTask>;
static var current: Float;
static var lastTime: Float;
static var lastFrameEnd: Float;
static var frame_tasks_sorted: Bool;
static var stopped: Bool;
static var vsync: Bool;
// Html5 target can update display frequency after some delay
#if kha_html5
static var onedifhz(get, never): Float;
static inline function get_onedifhz(): Float {
return 1.0 / Display.primary.frequency;
}
#else
static var onedifhz: Float;
#end
static var currentFrameTaskId: Int;
static var currentTimeTaskId: Int;
static var currentGroupId: Int;
static var DIF_COUNT = 3;
static var maxframetime = 0.5;
static var deltas: Array<Float>;
static var startTime: Float = 0;
static var activeTimeTask: TimeTask = null;
public static function init(): Void {
deltas = new Array<Float>();
for (i in 0...DIF_COUNT)
deltas[i] = 0;
stopped = true;
frame_tasks_sorted = true;
current = lastTime = lastFrameEnd = realTime();
currentFrameTaskId = 0;
currentTimeTaskId = 0;
currentGroupId = 0;
timeTasks = [];
pausedTimeTasks = [];
outdatedTimeTasks = [];
timeTasksScratchpad = [];
frameTasks = [];
toDeleteFrame = [];
}
public static function start(restartTimers: Bool = false): Void {
vsync = Window.get(0).vSynced;
#if !kha_html5
var hz = Display.primary != null ? Display.primary.frequency : 60;
if (hz >= 57 && hz <= 63)
hz = 60;
onedifhz = 1.0 / hz;
#end
stopped = false;
resetTime();
lastTime = realTime() - startTime;
for (i in 0...DIF_COUNT)
deltas[i] = 0;
if (restartTimers) {
for (timeTask in timeTasks) {
timeTask.paused = false;
}
for (frameTask in frameTasks) {
frameTask.paused = false;
}
}
}
public static function stop(): Void {
stopped = true;
}
public static function isStopped(): Bool {
return stopped;
}
static function warpTimeTasksBack(time: Float, tasks: Array<TimeTask>): Void {
for (timeTask in tasks) {
if (timeTask.start >= time) {
timeTask.next = timeTask.start;
}
else if (timeTask.period > 0) {
var sinceStart = time - timeTask.start;
var times = Math.ceil(sinceStart / timeTask.period);
timeTask.next = timeTask.start + times * timeTask.period;
}
}
}
public static function warp(time: Float): Void {
if (time < lastTime) {
current = time;
lastTime = time;
lastFrameEnd = time;
warpTimeTasksBack(time, outdatedTimeTasks);
warpTimeTasksBack(time, timeTasks);
for (task in outdatedTimeTasks) {
if (task.next >= time) {
timeTasksScratchpad.push(task);
}
}
for (task in timeTasksScratchpad) {
outdatedTimeTasks.remove(task);
}
for (task in timeTasksScratchpad) {
insertSorted(timeTasks, task);
}
while (timeTasksScratchpad.length > 0) {
timeTasksScratchpad.remove(timeTasksScratchpad[0]);
}
}
else if (time > lastTime) {
// TODO: Changing startTime line prevents clients from falling into a
// warp-forward-then-wait-for-systemtime-to-catch-up-loop that causes
// choppy movement (e.g. every 3rd frame forward 3 times).
// But it causes backwards jumps in originally constant movements.
// And on HTML5 packets are received while no frames are executed,
// which causes the client to overtakes the server and then move
// farther away with each packet while being unable to synch back
// (backwards warping is not allowed to change startTime).
startTime -= (time - lastTime);
current = time;
lastTime = time;
lastFrameEnd = time;
executeTimeTasks(time);
}
}
public static function executeFrame(): Void {
var real = realTime();
var now: Float = real - startTime;
var delta = now - lastTime;
var frameEnd: Float = lastFrameEnd;
if (delta >= 0) {
if (kha.netsync.Session.the() == null) {
// tdif = 1.0 / 60.0; //force fixed frame rate
if (delta > maxframetime) {
startTime += delta - maxframetime;
now = real - startTime;
delta = maxframetime;
frameEnd += delta;
}
else {
if (vsync) {
// var measured = delta;
// this is optimized not to run at exact speed
// but to run as fluid as possible
var frames = Math.round(delta / onedifhz);
if (frames < 1) {
return;
}
var realdif = frames * onedifhz;
delta = realdif;
for (i in 0...DIF_COUNT - 2) {
delta += deltas[i];
deltas[i] = deltas[i + 1];
}
delta += deltas[DIF_COUNT - 2];
delta /= DIF_COUNT;
deltas[DIF_COUNT - 2] = realdif;
frameEnd += delta;
// trace("Measured: " + measured + " Frames: " + frames + " Delta: " + delta + " ");
}
else {
for (i in 0...DIF_COUNT - 1) {
deltas[i] = deltas[i + 1];
}
deltas[DIF_COUNT - 1] = delta;
var next: Float = 0;
for (i in 0...DIF_COUNT) {
next += deltas[i];
}
next /= DIF_COUNT;
// delta = interpolated_delta; // average the frame end estimation
// lastTime = now;
frameEnd += next;
}
}
}
else {
frameEnd += delta;
}
lastTime = now;
if (!stopped) { // Stop simulation time
lastFrameEnd = frameEnd;
}
// Extend endpoint by paused time (individually paused tasks)
for (pausedTask in pausedTimeTasks) {
pausedTask.next += delta;
}
if (stopped) {
// Extend endpoint by paused time (running tasks)
for (timeTask in timeTasks) {
timeTask.next += delta;
}
}
executeTimeTasks(frameEnd);
// Maintain outdated task list
for (task in outdatedTimeTasks) {
if (task.next < frameEnd - timeWarpSaveTime) {
timeTasksScratchpad.push(task);
}
}
for (task in timeTasksScratchpad) {
outdatedTimeTasks.remove(task);
}
while (timeTasksScratchpad.length > 0) {
timeTasksScratchpad.remove(timeTasksScratchpad[0]);
}
}
current = frameEnd;
sortFrameTasks();
for (frameTask in frameTasks) {
if (!stopped && !frameTask.paused && frameTask.active) {
if (!frameTask.task())
frameTask.active = false;
}
}
for (frameTask in frameTasks) {
if (!frameTask.active) {
toDeleteFrame.push(frameTask);
}
}
while (toDeleteFrame.length > 0) {
frameTasks.remove(toDeleteFrame.pop());
}
}
static function executeTimeTasks(until: Float) {
while (timeTasks.length > 0) {
activeTimeTask = timeTasks[0];
if (activeTimeTask.next <= until) {
current = activeTimeTask.next;
activeTimeTask.next += activeTimeTask.period;
timeTasks.remove(activeTimeTask);
if (activeTimeTask.active && activeTimeTask.task()) {
if (activeTimeTask.period > 0
&& (activeTimeTask.duration == 0 || activeTimeTask.duration >= activeTimeTask.start + activeTimeTask.next)) {
insertSorted(timeTasks, activeTimeTask);
}
else {
archiveTimeTask(activeTimeTask, until);
}
}
else {
activeTimeTask.active = false;
archiveTimeTask(activeTimeTask, until);
}
}
else {
break;
}
}
activeTimeTask = null;
}
static function archiveTimeTask(timeTask: TimeTask, frameEnd: Float) {
#if sys_server
if (timeTask.next > frameEnd - timeWarpSaveTime) {
outdatedTimeTasks.push(timeTask);
}
#end
}
/**
* An approximation of the amount of time (in fractional seconds) that elapsed while the game was active.
* This value is optimized for achieving smooth framerates.
*/
public static function time(): Float {
return current;
}
/**
* The amount of time (in fractional seconds) that elapsed since the game started.
*/
public static function realTime(): Float {
return System.time;
}
public static function resetTime(): Void {
var now = System.time;
var dif = now - startTime;
startTime = now;
for (timeTask in timeTasks) {
timeTask.start -= dif;
timeTask.next -= dif;
}
for (i in 0...DIF_COUNT)
deltas[i] = 0;
current = 0;
lastTime = 0;
lastFrameEnd = 0;
}
public static function addBreakableFrameTask(task: Void->Bool, priority: Int): Int {
frameTasks.push(new FrameTask(task, priority, ++currentFrameTaskId));
frame_tasks_sorted = false;
return currentFrameTaskId;
}
public static function addFrameTask(task: Void->Void, priority: Int): Int {
return addBreakableFrameTask(function() {
task();
return true;
}, priority);
}
public static function pauseFrameTask(id: Int, paused: Bool): Void {
for (frameTask in frameTasks) {
if (frameTask.id == id) {
frameTask.paused = paused;
break;
}
}
}
public static function removeFrameTask(id: Int): Void {
for (frameTask in frameTasks) {
if (frameTask.id == id) {
frameTask.active = false;
break;
}
}
}
public static function generateGroupId(): Int {
return ++currentGroupId;
}
public static function addBreakableTimeTaskToGroup(groupId: Int, task: Void->Bool, start: Float, period: Float = 0, duration: Float = 0): Int {
var t = new TimeTask();
t.active = true;
t.task = task;
t.id = ++currentTimeTaskId;
t.groupId = groupId;
t.start = current + start;
t.period = 0;
if (period != 0)
t.period = period;
t.duration = 0; // infinite
if (duration != 0)
t.duration = t.start + duration;
t.next = t.start;
insertSorted(timeTasks, t);
return t.id;
}
public static function addTimeTaskToGroup(groupId: Int, task: Void->Void, start: Float, period: Float = 0, duration: Float = 0): Int {
return addBreakableTimeTaskToGroup(groupId, function() {
task();
return true;
}, start, period, duration);
}
public static function addBreakableTimeTask(task: Void->Bool, start: Float, period: Float = 0, duration: Float = 0): Int {
return addBreakableTimeTaskToGroup(0, task, start, period, duration);
}
public static function addTimeTask(task: Void->Void, start: Float, period: Float = 0, duration: Float = 0): Int {
return addTimeTaskToGroup(0, task, start, period, duration);
}
static function getTimeTask(id: Int): TimeTask {
if (activeTimeTask != null && activeTimeTask.id == id)
return activeTimeTask;
for (timeTask in timeTasks) {
if (timeTask.id == id) {
return timeTask;
}
}
for (timeTask in pausedTimeTasks) {
if (timeTask.id == id) {
return timeTask;
}
}
return null;
}
public static function pauseTimeTask(id: Int, paused: Bool): Void {
var timeTask = getTimeTask(id);
if (timeTask != null) {
pauseRunningTimeTask(timeTask, paused);
}
if (activeTimeTask != null && activeTimeTask.id == id) {
activeTimeTask.paused = paused;
}
}
static function pauseRunningTimeTask(timeTask: TimeTask, paused: Bool): Void {
timeTask.paused = paused;
if (paused) {
timeTasks.remove(timeTask);
pausedTimeTasks.push(timeTask);
}
else {
insertSorted(timeTasks, timeTask);
pausedTimeTasks.remove(timeTask);
}
}
public static function pauseTimeTasks(groupId: Int, paused: Bool): Void {
if (paused) {
for (timeTask in timeTasks) {
if (timeTask.groupId == groupId) {
pauseRunningTimeTask(timeTask, paused);
}
}
}
else {
for (timeTask in pausedTimeTasks) {
if (timeTask.groupId == groupId) {
pauseRunningTimeTask(timeTask, paused);
}
}
}
if (activeTimeTask != null && activeTimeTask.groupId == groupId) {
activeTimeTask.paused = paused;
}
}
public static function removeTimeTask(id: Int): Void {
var timeTask = getTimeTask(id);
if (timeTask != null) {
timeTask.active = false;
timeTasks.remove(timeTask);
}
}
public static function removeTimeTasks(groupId: Int): Void {
for (timeTask in timeTasks) {
if (timeTask.groupId == groupId) {
timeTask.active = false;
timeTasksScratchpad.push(timeTask);
}
}
for (timeTask in timeTasksScratchpad) {
timeTasks.remove(timeTask);
}
while (timeTasksScratchpad.length > 0) {
timeTasksScratchpad.remove(timeTasksScratchpad[0]);
}
if (activeTimeTask != null && activeTimeTask.groupId == groupId) {
activeTimeTask.active = false;
}
}
public static function numTasksInSchedule(): Int {
return timeTasks.length + frameTasks.length;
}
static function insertSorted(list: Array<TimeTask>, task: TimeTask) {
for (i in 0...list.length) {
if (list[i].next > task.next) {
list.insert(i, task);
return;
}
}
list.push(task);
}
static function sortFrameTasks(): Void {
if (frame_tasks_sorted)
return;
frameTasks.sort(function(a: FrameTask, b: FrameTask): Int {
return a.priority > b.priority ? 1 : ((a.priority < b.priority) ? -1 : 0);
});
frame_tasks_sorted = true;
}
}