From 86fb39461732d3d688d3ac13bcb1e60ae2af7817 Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Mon, 1 Jan 2024 12:29:04 -0500 Subject: [PATCH] Redo sleeping as an opcode - Remove `sleep` from DMStandard. - Two new opcodes Sleep and BackgroundSleep (which is just `sleep -1`, much faster than popping a `float` off for background sleeps). - Create new lightweight async proc state for sleeping. - Compiler/runtime adjustments to handle new opcodes. --- DMCompiler/Bytecode/DreamProcOpcode.cs | 2 + DMCompiler/Compiler/DM/DMAST.cs | 17 ++++ DMCompiler/Compiler/DM/DMLexer.cs | 1 + DMCompiler/Compiler/DM/DMParser.cs | 19 +++++ DMCompiler/DM/DMProc.cs | 21 +++-- DMCompiler/DM/Visitors/DMASTSimplifier.cs | 4 + DMCompiler/DM/Visitors/DMProcBuilder.cs | 16 ++++ DMCompiler/DMStandard/_Standard.dm | 1 - OpenDreamRuntime/Procs/AsyncNativeProc.cs | 4 +- OpenDreamRuntime/Procs/AsyncProcState.cs | 5 ++ OpenDreamRuntime/Procs/DMOpcodeHandlers.cs | 81 ++++++++++++++++++- OpenDreamRuntime/Procs/DMProc.cs | 2 + .../Procs/Native/DreamProcNative.cs | 1 - .../Procs/Native/DreamProcNativeRoot.cs | 10 --- OpenDreamRuntime/Procs/ProcDecoder.cs | 1 + OpenDreamRuntime/Procs/ProcScheduler.cs | 20 +++-- OpenDreamShared/Compiler/Token.cs | 1 + 17 files changed, 175 insertions(+), 31 deletions(-) create mode 100644 OpenDreamRuntime/Procs/AsyncProcState.cs diff --git a/DMCompiler/Bytecode/DreamProcOpcode.cs b/DMCompiler/Bytecode/DreamProcOpcode.cs index 5c7a1599a8..7e59861f84 100644 --- a/DMCompiler/Bytecode/DreamProcOpcode.cs +++ b/DMCompiler/Bytecode/DreamProcOpcode.cs @@ -139,6 +139,8 @@ public enum DreamProcOpcode : byte { Log = 0x81, LogE = 0x82, Abs = 0x83, + [OpcodeMetadata(stackDelta: -1)] Sleep = 0x84, + BackgroundSleep = 0x85, } /// diff --git a/DMCompiler/Compiler/DM/DMAST.cs b/DMCompiler/Compiler/DM/DMAST.cs index 9ddaadd626..a325a21bee 100644 --- a/DMCompiler/Compiler/DM/DMAST.cs +++ b/DMCompiler/Compiler/DM/DMAST.cs @@ -78,6 +78,10 @@ public void VisitProcStatementSpawn(DMASTProcStatementSpawn statementSpawn) { throw new NotImplementedException(); } + public void VisitProcStatementSleep(DMASTProcStatementSleep statementSleep) { + throw new NotImplementedException(); + } + public void VisitProcStatementIf(DMASTProcStatementIf statementIf) { throw new NotImplementedException(); } @@ -920,6 +924,19 @@ public override void Visit(DMASTVisitor visitor) { } } + public sealed class DMASTProcStatementSleep : DMASTProcStatement { + public DMASTExpression Delay; + + public DMASTProcStatementSleep(Location location, DMASTExpression delay) : + base(location) { + Delay = delay; + } + + public override void Visit(DMASTVisitor visitor) { + visitor.VisitProcStatementSleep(this); + } + } + public sealed class DMASTProcStatementSpawn : DMASTProcStatement { public DMASTExpression Delay; public readonly DMASTProcBlockInner Body; diff --git a/DMCompiler/Compiler/DM/DMLexer.cs b/DMCompiler/Compiler/DM/DMLexer.cs index fbd0741f6a..4ecf96b496 100644 --- a/DMCompiler/Compiler/DM/DMLexer.cs +++ b/DMCompiler/Compiler/DM/DMLexer.cs @@ -60,6 +60,7 @@ public sealed class DMLexer : TokenLexer { { "call", TokenType.DM_Call }, { "call_ext", TokenType.DM_Call}, { "spawn", TokenType.DM_Spawn }, + { "sleep", TokenType.DM_Sleep }, { "goto", TokenType.DM_Goto }, { "step", TokenType.DM_Step }, { "try", TokenType.DM_Try }, diff --git a/DMCompiler/Compiler/DM/DMParser.cs b/DMCompiler/Compiler/DM/DMParser.cs index 7d667135f4..b96a97a0b4 100644 --- a/DMCompiler/Compiler/DM/DMParser.cs +++ b/DMCompiler/Compiler/DM/DMParser.cs @@ -100,6 +100,7 @@ public DMParser(DMLexer lexer) : base(lexer) { TokenType.DM_Null, TokenType.DM_Switch, TokenType.DM_Spawn, + TokenType.DM_Sleep, TokenType.DM_Do, TokenType.DM_While, TokenType.DM_For, @@ -715,6 +716,7 @@ public DMASTFile File() { procStatement ??= Switch(); procStatement ??= Continue(); procStatement ??= Break(); + procStatement ??= Sleep(); procStatement ??= Spawn(); procStatement ??= While(); procStatement ??= DoWhile(); @@ -1059,6 +1061,23 @@ private DMASTProcStatementSet[] ProcSetEnd(bool allowMultiple) { } } + public DMASTProcStatementSleep? Sleep() { + var loc = Current().Location; + + if (Check(TokenType.DM_Sleep)) { + Whitespace(); + bool hasParenthesis = Check(TokenType.DM_LeftParenthesis); + Whitespace(); + DMASTExpression? delay = Expression(); + if (delay == null) Error("Expected delay to sleep for"); + if (hasParenthesis) ConsumeRightParenthesis(); + + return new DMASTProcStatementSleep(loc, delay ?? new DMASTConstantInteger(loc, 0)); + } else { + return null; + } + } + public DMASTProcStatementIf? If() { var loc = Current().Location; diff --git a/DMCompiler/DM/DMProc.cs b/DMCompiler/DM/DMProc.cs index 4f7320cb1e..3e44a85266 100644 --- a/DMCompiler/DM/DMProc.cs +++ b/DMCompiler/DM/DMProc.cs @@ -459,20 +459,19 @@ public void MarkLoopContinue(string loopLabel) { AddLabel($"{loopLabel}_continue"); } - public void BackgroundSleep() { - // TODO This seems like a bad way to handle background, doesn't it? - - if ((Attributes & ProcAttributes.Background) == ProcAttributes.Background) { - if (!DMObjectTree.TryGetGlobalProc("sleep", out var sleepProc)) { - throw new CompileErrorException(Location, "Cannot do a background sleep without a sleep proc"); - } - - PushFloat(-1); // argument given to sleep() - Call(DMReference.CreateGlobalProc(sleepProc.Id), DMCallArgumentsType.FromStack, 1); - Pop(); // Pop the result of the sleep call + public void SleepDelayPushed() => WriteOpcode(DreamProcOpcode.Sleep); + + public void Sleep(float delay) { + if(delay == -1.0f) // yielding + WriteOpcode(DreamProcOpcode.BackgroundSleep); + else { + PushFloat(delay); + WriteOpcode(DreamProcOpcode.Sleep); } } + public void BackgroundSleep() => WriteOpcode(DreamProcOpcode.BackgroundSleep); + public void LoopJumpToStart(string loopLabel) { BackgroundSleep(); Jump($"{loopLabel}_start"); diff --git a/DMCompiler/DM/Visitors/DMASTSimplifier.cs b/DMCompiler/DM/Visitors/DMASTSimplifier.cs index 77c8b7b371..d3aaea9794 100644 --- a/DMCompiler/DM/Visitors/DMASTSimplifier.cs +++ b/DMCompiler/DM/Visitors/DMASTSimplifier.cs @@ -124,6 +124,10 @@ public void VisitProcStatementSpawn(DMASTProcStatementSpawn statementSpawn) { statementSpawn.Body.Visit(this); } + public void VisitProcStatementSleep(DMASTProcStatementSleep statementSleep) { + SimplifyExpression(ref statementSleep.Delay); + } + public void VisitProcStatementGoto(DMASTProcStatementGoto statementGoto) { } diff --git a/DMCompiler/DM/Visitors/DMProcBuilder.cs b/DMCompiler/DM/Visitors/DMProcBuilder.cs index 14e540c08f..31ddc228c6 100644 --- a/DMCompiler/DM/Visitors/DMProcBuilder.cs +++ b/DMCompiler/DM/Visitors/DMProcBuilder.cs @@ -121,6 +121,7 @@ public void ProcessStatement(DMASTProcStatement statement) { case DMASTProcStatementBreak statementBreak: ProcessStatementBreak(statementBreak); break; case DMASTProcStatementDel statementDel: ProcessStatementDel(statementDel); break; case DMASTProcStatementSpawn statementSpawn: ProcessStatementSpawn(statementSpawn); break; + case DMASTProcStatementSleep statementSleep: ProcessStatementSleep(statementSleep); break; case DMASTProcStatementReturn statementReturn: ProcessStatementReturn(statementReturn); break; case DMASTProcStatementIf statementIf: ProcessStatementIf(statementIf); break; case DMASTProcStatementFor statementFor: ProcessStatementFor(statementFor); break; @@ -317,6 +318,21 @@ public void ProcessStatementSpawn(DMASTProcStatementSpawn statementSpawn) { _proc.AddLabel(afterSpawnLabel); } + public void ProcessStatementSleep(DMASTProcStatementSleep statementSleep) { + var expr = DMExpression.Create(_dmObject, _proc, statementSleep.Delay); + if (expr.TryAsConstant(out var constant)) { + if (constant is Number constantNumber) { + _proc.Sleep(constantNumber.Value); + return; + } + + constant.EmitPushValue(_dmObject, _proc); + } else + expr.EmitPushValue(_dmObject, _proc); + + _proc.SleepDelayPushed(); + } + public void ProcessStatementVarDeclaration(DMASTProcStatementVarDeclaration varDeclaration) { if (varDeclaration.IsGlobal) { return; } //Currently handled by DMObjectBuilder diff --git a/DMCompiler/DMStandard/_Standard.dm b/DMCompiler/DMStandard/_Standard.dm index 8c730da05b..6447063a5d 100644 --- a/DMCompiler/DMStandard/_Standard.dm +++ b/DMCompiler/DMStandard/_Standard.dm @@ -80,7 +80,6 @@ proc/roll(ndice = 1, sides) proc/round(A, B) proc/sha1(input) proc/shutdown(Addr,Natural = 0) -proc/sleep(Delay) proc/sorttext(T1, T2) proc/sorttextEx(T1, T2) proc/sound(file, repeat = 0, wait, channel, volume) diff --git a/OpenDreamRuntime/Procs/AsyncNativeProc.cs b/OpenDreamRuntime/Procs/AsyncNativeProc.cs index cc9576358f..22c703cb31 100644 --- a/OpenDreamRuntime/Procs/AsyncNativeProc.cs +++ b/OpenDreamRuntime/Procs/AsyncNativeProc.cs @@ -9,7 +9,7 @@ namespace OpenDreamRuntime.Procs { public sealed class AsyncNativeProc : DreamProc { - public sealed class State : ProcState { + public sealed class State : AsyncProcState { public static readonly Stack Pool = new(); // IoC dependencies instead of proc fields because _proc can be null @@ -52,7 +52,7 @@ public void Initialize(AsyncNativeProc? proc, Func> task } // Used to avoid reentrant resumptions in our proc - public void SafeResume() { + public override void SafeResume() { if (_inResume) { return; } diff --git a/OpenDreamRuntime/Procs/AsyncProcState.cs b/OpenDreamRuntime/Procs/AsyncProcState.cs new file mode 100644 index 0000000000..b08d014d3c --- /dev/null +++ b/OpenDreamRuntime/Procs/AsyncProcState.cs @@ -0,0 +1,5 @@ +namespace OpenDreamRuntime.Procs { + public abstract class AsyncProcState : ProcState { + public abstract void SafeResume(); + } +} diff --git a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs index 0d600af68a..816932a0bf 100644 --- a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs +++ b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs @@ -12,6 +12,8 @@ using OpenDreamRuntime.Resources; using OpenDreamShared.Dream; using Robust.Shared.Random; + +using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute; using Vector4 = Robust.Shared.Maths.Vector4; namespace OpenDreamRuntime.Procs { @@ -167,7 +169,7 @@ public static ProcStatus CreateObject(DMProcState state) { var val = state.Pop(); if (!val.TryGetValueAsType(out var objectType)) { if (val.TryGetValueAsString(out var pathString)) { - if (!state.Proc.ObjectTree.TryGetTreeEntry(new DreamPath(pathString), out objectType)) { + if (!state.Proc.ObjectTree.TryGetTreeEntry(pathString, out objectType)) { ThrowCannotCreateUnknownObject(val); } } else { @@ -1713,6 +1715,83 @@ async void Wait() { return ProcStatus.Continue; } + public static ProcStatus Sleep(DMProcState state) { + state.Pop().TryGetValueAsFloat(out var delay); + return SleepCore( + state, + state.ProcScheduler.CreateDelay(delay)); + } + + public static ProcStatus BackgroundSleep(DMProcState state) => SleepCore( + state, + state.ProcScheduler.CreateDelayTicks(-1)); + + static ProcStatus SleepCore(DMProcState state, Task delay) { + if (delay.IsCompleted) + return ProcStatus.Continue; // fast path, skip state creation + + if (!SleepState.Pool.TryPop(out var sleepState)) { + sleepState = new SleepState(); + } + + return sleepState.Initialize(state.Thread, state.Proc, delay); + } + + // "proc state" we just need something to hold the delay task + sealed class SleepState : AsyncProcState { + public static readonly Stack Pool = new(); + + [Dependency] public readonly ProcScheduler ProcScheduler = null!; + + DreamProc? _proc; + Task? _task; + bool inResume; + + public SleepState() { + IoCManager.InjectDependencies(this); + } + + public ProcStatus Initialize(DreamThread thread, DMProc proc, Task delay) { + Thread = thread; + _proc = proc; + _task = ProcScheduler.Schedule(this, delay); + thread.PushProcState(this); + return thread.HandleDefer(); + } + + public override void Dispose() { + base.Dispose(); + Thread = null!; + _proc = null; + _task = null; + Pool.Push(this); + } + + public override DreamProc? Proc => _proc; + + public override void AppendStackFrame(StringBuilder builder) { + builder.Append("/proc/sleep"); + } + + // a sleep is always the top of a thread so it's always safe to resume + public override void SafeResume() => Thread.Resume(); + + public override ProcStatus Resume() { + if (_task!.IsCompleted) { + // read before we get disposed when popped off + var exception = _task.Exception; + Thread.PopProcState(); + if (exception != null) { + throw exception; + } + + return ProcStatus.Returned; + } + + return Thread.HandleDefer(); + } + } + public static ProcStatus DebuggerBreakpoint(DMProcState state) { return state.DebugManager.HandleBreakpoint(state); } diff --git a/OpenDreamRuntime/Procs/DMProc.cs b/OpenDreamRuntime/Procs/DMProc.cs index b8c155ed82..452acc2cfd 100644 --- a/OpenDreamRuntime/Procs/DMProc.cs +++ b/OpenDreamRuntime/Procs/DMProc.cs @@ -249,6 +249,8 @@ public sealed class DMProcState : ProcState { {DreamProcOpcode.Locate, DMOpcodeHandlers.Locate}, {DreamProcOpcode.IsNull, DMOpcodeHandlers.IsNull}, {DreamProcOpcode.Spawn, DMOpcodeHandlers.Spawn}, + {DreamProcOpcode.Sleep, DMOpcodeHandlers.Sleep}, + {DreamProcOpcode.BackgroundSleep, DMOpcodeHandlers.BackgroundSleep}, {DreamProcOpcode.OutputReference, DMOpcodeHandlers.OutputReference}, {DreamProcOpcode.Output, DMOpcodeHandlers.Output}, {DreamProcOpcode.JumpIfNullDereference, DMOpcodeHandlers.JumpIfNullDereference}, diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNative.cs b/OpenDreamRuntime/Procs/Native/DreamProcNative.cs index c078826a2e..c7f347c6c5 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNative.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNative.cs @@ -79,7 +79,6 @@ public static void SetupNativeProcs(DreamObjectTree objectTree) { objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_round); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_sha1); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_shutdown); - objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_sleep); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_sorttext); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_sorttextEx); objectTree.SetGlobalNativeProc(DreamProcNativeRoot.NativeProc_sound); diff --git a/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs b/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs index 4489bcd9a6..c21e9e6666 100644 --- a/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs +++ b/OpenDreamRuntime/Procs/Native/DreamProcNativeRoot.cs @@ -2145,16 +2145,6 @@ public static DreamValue NativeProc_shutdown(NativeProc.Bundle bundle, DreamObje return DreamValue.Null; } - [DreamProc("sleep")] - [DreamProcParameter("Delay", Type = DreamValueTypeFlag.Float)] - public static async Task NativeProc_sleep(AsyncNativeProc.State state) { - state.GetArgument(0, "Delay").TryGetValueAsFloat(out float delay); - - await state.ProcScheduler.CreateDelay(delay); - - return DreamValue.Null; - } - [DreamProc("sorttext")] [DreamProcParameter("T1", Type = DreamValueTypeFlag.String)] [DreamProcParameter("T2", Type = DreamValueTypeFlag.String)] diff --git a/OpenDreamRuntime/Procs/ProcDecoder.cs b/OpenDreamRuntime/Procs/ProcDecoder.cs index 3386663015..6a01d4e06e 100644 --- a/OpenDreamRuntime/Procs/ProcDecoder.cs +++ b/OpenDreamRuntime/Procs/ProcDecoder.cs @@ -124,6 +124,7 @@ public ITuple DecodeInstruction() { case DreamProcOpcode.PickWeighted: case DreamProcOpcode.PickUnweighted: case DreamProcOpcode.Spawn: + case DreamProcOpcode.Sleep: case DreamProcOpcode.BooleanOr: case DreamProcOpcode.BooleanAnd: case DreamProcOpcode.SwitchCase: diff --git a/OpenDreamRuntime/Procs/ProcScheduler.cs b/OpenDreamRuntime/Procs/ProcScheduler.cs index 2daee6f2a9..5c1f39d439 100644 --- a/OpenDreamRuntime/Procs/ProcScheduler.cs +++ b/OpenDreamRuntime/Procs/ProcScheduler.cs @@ -22,22 +22,32 @@ namespace OpenDreamRuntime.Procs; public sealed partial class ProcScheduler { - private readonly HashSet _sleeping = new(); - private readonly Queue _scheduled = new(); - private AsyncNativeProc.State? _current; + private readonly HashSet _sleeping = new(); + private readonly Queue _scheduled = new(); + private AsyncProcState? _current; public bool HasProcsQueued => _scheduled.Count > 0 || _deferredTasks.Count > 0; public Task Schedule(AsyncNativeProc.State state, Func> taskFunc) { async Task Foo() { state.Result = await taskFunc(state); - if (!_sleeping.Remove(state)) + } + + return Schedule( + state, + Foo()); + } + + public Task Schedule(AsyncProcState state, Task asyncTask) { + async Task Bar() { + await asyncTask; + if(!_sleeping.Remove(state)) return; _scheduled.Enqueue(state); } - var task = Foo(); + var task = Bar(); if (!task.IsCompleted) // No need to schedule the proc if it's already finished _sleeping.Add(state); diff --git a/OpenDreamShared/Compiler/Token.cs b/OpenDreamShared/Compiler/Token.cs index 979dfd9840..d55019ebf7 100644 --- a/OpenDreamShared/Compiler/Token.cs +++ b/OpenDreamShared/Compiler/Token.cs @@ -121,6 +121,7 @@ public enum TokenType : byte { DM_Slash, DM_SlashEquals, DM_Spawn, + DM_Sleep, DM_Star, DM_StarEquals, DM_StarStar,