Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compiletime rgb() evaluation #2115

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions DMCompiler/DM/Expressions/Builtins.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,53 @@ public override void EmitPushValue(ExpressionContext ctx) {

ctx.Proc.Rgb(argInfo.Type, argInfo.StackSize);
}

// TODO: This needs to have full parity with the rgb opcode. This is a simplified implementation for the most common case rgb(R, G, B)
public override bool TryAsConstant(DMCompiler compiler, [NotNullWhen(true)] out Constant? constant) {
(string?, float?)[] values = new (string?, float?)[arguments.Length];

bool validArgs = true;

if (arguments.Length < 3 || arguments.Length > 5) {
compiler.Emit(WarningCode.BadExpression, Location, $"rgb: expected 3 to 5 arguments (found {arguments.Length})");
constant = null;
return false;
}

for (var index = 0; index < arguments.Expressions.Length; index++) {
var (name, expr) = arguments.Expressions[index];
if (!expr.TryAsConstant(compiler, out var constExpr)) {
constant = null;
return false;
}

if (constExpr is not Number num) {
validArgs = false;
values[index] = (name, null);
continue;
}

values[index] = (name, num.Value);
}

if (!validArgs) {
compiler.Emit(WarningCode.FallbackBuiltinArgument, Location,
"Non-numerical rgb argument(s) will always return \"00\"");
}

string result;
try {
result = SharedOperations.ParseRgb(values);
} catch (Exception e) {
compiler.Emit(WarningCode.BadExpression, Location, e.Message);
constant = null;
return false;
}

constant = new String(Location, result);

return true;
}
}

// pick(prob(50);x, prob(200);y)
Expand Down
4 changes: 4 additions & 0 deletions DMCompiler/DMCompiler.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
<ItemGroup>
<DMStandard Include="DMStandard\**" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\RobustToolbox\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
</ItemGroup>

<Target Name="CopyDMStandard" AfterTargets="AfterBuild">
<Copy SourceFiles="@(DMStandard)" DestinationFiles="@(DMStandard->'$(OutDir)\DMStandard\%(RecursiveDir)%(Filename)%(Extension)')" />
Expand Down
41 changes: 41 additions & 0 deletions DMCompiler/Optimizer/CompactorOptimizations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -375,4 +375,45 @@ public void Apply(DMCompiler compiler, List<IAnnotatedBytecode> input, int index
}
}

// PushNFloats [count] [float] ... [float]
// Rgb [argType] [count]
// -> PushString [result]
// Only works when [argType] is FromStack and the [count] of both opcodes matches
internal sealed class EvalRgb : IOptimization {
public OptPass OptimizationPass => OptPass.ListCompactor;

public ReadOnlySpan<DreamProcOpcode> GetOpcodes() {
return [
DreamProcOpcode.PushNFloats,
DreamProcOpcode.Rgb
];
}

public bool CheckPreconditions(List<IAnnotatedBytecode> input, int index) {
var floatCount = ((AnnotatedBytecodeInstruction)input[index]).GetArg<AnnotatedBytecodeInteger>(0).Value;
var rgbInst = (AnnotatedBytecodeInstruction)input[index + 1];
var argType = rgbInst.GetArg<AnnotatedBytecodeArgumentType>(0).Value;
var stackDelta = rgbInst.GetArg<AnnotatedBytecodeStackDelta>(1).Delta;

return argType == DMCallArgumentsType.FromStack && floatCount == stackDelta;
}

public void Apply(DMCompiler compiler, List<IAnnotatedBytecode> input, int index) {
var floats = (AnnotatedBytecodeInstruction)(input[index]);
var floatArgs = floats.GetArgs();
(string?, float?)[] values = new (string?, float?)[floatArgs.Count - 1];
for (int i = 1; i < floatArgs.Count; i++) { // skip the first value since it's the [count] of floats
values[i - 1] = (null, ((AnnotatedBytecodeFloat)floatArgs[i]).Value);
}

var resultStr = SharedOperations.ParseRgb(values);
var resultId = compiler.DMObjectTree.AddString(resultStr);

List<IAnnotatedBytecode> args = [new AnnotatedBytecodeString(resultId, floats.Location)];

input.RemoveRange(index, 2);
input.Insert(index, new AnnotatedBytecodeInstruction(DreamProcOpcode.PushString, 1, args));
}
}

#endregion
105 changes: 105 additions & 0 deletions DMCompiler/SharedOperations.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
using Robust.Shared.Maths;

namespace DMCompiler;

Expand Down Expand Up @@ -61,4 +62,108 @@
public static float Abs(float a) {
return MathF.Abs(a);
}

public enum ColorSpace {
RGB = 0,
HSV = 1,
HSL = 2
}

public static string ParseRgb((string? Name, float? Value)[] arguments) {
string result;
float? color1 = null;
float? color2 = null;
float? color3 = null;
float? a = null;
ColorSpace space = ColorSpace.RGB;

if (arguments[0].Name is null) {
if (arguments.Length is < 3 or > 5)
throw new Exception("Expected 3 to 5 arguments for rgb()");

color1 = arguments[0].Value;
color2 = arguments[1].Value;
color3 = arguments[2].Value;
a = (arguments.Length >= 4) ? arguments[3].Value : null;
if (arguments.Length == 5)
space = arguments[4].Value is null ? ColorSpace.RGB : (ColorSpace)(int)arguments[4].Value!;
} else {
foreach (var arg in arguments) {
var name = arg.Name ?? string.Empty;

if (name.StartsWith("r", StringComparison.InvariantCultureIgnoreCase) && color1 is null) {
color1 = arg.Value;
space = ColorSpace.RGB;
} else if (name.StartsWith("g", StringComparison.InvariantCultureIgnoreCase) && color2 is null) {
color2 = arg.Value;
space = ColorSpace.RGB;
} else if (name.StartsWith("b", StringComparison.InvariantCultureIgnoreCase) && color3 is null) {
color3 = arg.Value;
space = ColorSpace.RGB;
} else if (name.StartsWith("h", StringComparison.InvariantCultureIgnoreCase) && color1 is null) {
color1 = arg.Value;
space = ColorSpace.HSV;
} else if (name.StartsWith("s", StringComparison.InvariantCultureIgnoreCase) && color2 is null) {
color2 = arg.Value;
space = ColorSpace.HSV;
} else if (name.StartsWith("v", StringComparison.InvariantCultureIgnoreCase) && color3 is null) {
color3 = arg.Value;
space = ColorSpace.HSV;
} else if (name.StartsWith("l", StringComparison.InvariantCultureIgnoreCase) && color3 is null) {
color3 = arg.Value;
space = ColorSpace.HSL;
} else if (name.StartsWith("a", StringComparison.InvariantCultureIgnoreCase) && a is null)
a = arg.Value;
else if (name == "space" && space == default)
space = (ColorSpace)(int)arg.Value!;
else
throw new Exception($"Invalid or double arg \"{name}\"");
}
}

color1 ??= 0;
color2 ??= 0;
color3 ??= 0;
byte aValue = a is null ? (byte)255 : (byte)Math.Clamp((int)a, 0, 255);
Color color;

switch (space) {
case ColorSpace.RGB: {
byte r = (byte)Math.Clamp(color1.Value, 0, 255);
byte g = (byte)Math.Clamp(color2.Value, 0, 255);
byte b = (byte)Math.Clamp(color3.Value, 0, 255);

color = new Color(r, g, b, aValue);
break;
}
case ColorSpace.HSV: {
// TODO: Going beyond the max defined in the docs returns a different value. Don't know why.
float h = Math.Clamp(color1.Value, 0, 360) / 360f;
float s = Math.Clamp(color2.Value, 0, 100) / 100f;
float v = Math.Clamp(color3.Value, 0, 100) / 100f;

color = Color.FromHsv((h, s, v, aValue / 255f));
break;
}
case ColorSpace.HSL: {
float h = Math.Clamp(color1.Value, 0, 360) / 360f;
float s = Math.Clamp(color2.Value, 0, 100) / 100f;
float l = Math.Clamp(color3.Value, 0, 100) / 100f;

color = Color.FromHsl((h, s, l, aValue / 255f));
break;
}
default:
throw new Exception($"Unimplemented color space {space}");
}

// TODO: There is a difference between passing null and not passing a fourth arg at all
if (a is null) {

Check notice

Code scanning / InspectCode

'if' statement can be rewritten as '?:' expression Note

Convert into '?:' expression
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The huge ternary is less readable, so no.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea I think this is a notice we should turn off

result = $"#{color.RByte:X2}{color.GByte:X2}{color.BByte:X2}".ToLower();
} else {
result = $"#{color.RByte:X2}{color.GByte:X2}{color.BByte:X2}{color.AByte:X2}".ToLower();
}

return result;
}
}
118 changes: 41 additions & 77 deletions OpenDreamRuntime/Procs/DMOpcodeHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1947,120 +1947,84 @@ public static ProcStatus Rgb(DMProcState state) {
var argumentValues = state.PopCount(argumentInfo.StackSize);
var arguments = state.CollectProcArguments(argumentValues, argumentInfo.Type, argumentInfo.StackSize);

DreamValue color1 = default;
DreamValue color2 = default;
DreamValue color3 = default;
DreamValue a = DreamValue.Null;
ColorHelpers.ColorSpace space = ColorHelpers.ColorSpace.RGB;

if (arguments.Item1 != null) {
string result = "#000000";
if (arguments.Item1 is not null) {
if (arguments.Item1.Length is < 3 or > 5)
throw new Exception("Expected 3 to 5 arguments for rgb()");
(string?, float?)[] values = new (string?, float?)[arguments.Item1.Length];
for (int i = 0; i < arguments.Item1.Length; i++) {
var val = arguments.Item1[i].UnsafeGetValueAsFloat();
values[i] = (null, val);
}

color1 = arguments.Item1[0];
color2 = arguments.Item1[1];
color3 = arguments.Item1[2];
a = (arguments.Item1.Length >= 4) ? arguments.Item1[3] : DreamValue.Null;
if (arguments.Item1.Length == 5)
space = (ColorHelpers.ColorSpace)(int)arguments.Item1[4].UnsafeGetValueAsFloat();
result = SharedOperations.ParseRgb(values);
} else if (arguments.Item2 != null) {
if (arguments.Item2.Count is < 3 or > 5)
throw new Exception("Expected 3 to 5 arguments for rgb()");
(string?, float?)[] values = new (string?, float?)[5];
DreamValue color1 = default;
DreamValue color2 = default;
DreamValue color3 = default;
DreamValue a = DreamValue.Null;
SharedOperations.ColorSpace space = SharedOperations.ColorSpace.RGB;
foreach (var arg in arguments.Item2) {
if (arg.Key.TryGetValueAsInteger(out var position)) {
switch (position) {
case 1: color1 = arg.Value; break;
case 2: color2 = arg.Value; break;
case 3: color3 = arg.Value; break;
case 4: a = arg.Value; break;
case 5: space = (ColorHelpers.ColorSpace)(int)arg.Value.UnsafeGetValueAsFloat(); break;
case 1: color1 = arg.Value; continue;
case 2: color2 = arg.Value; continue;
case 3: color3 = arg.Value; continue;
case 4: a = arg.Value; continue;
case 5: space = (SharedOperations.ColorSpace)(int)arg.Value.UnsafeGetValueAsFloat(); continue;
default: throw new Exception($"Invalid argument key {position}");
}
} else {
var name = arg.Key.MustGetValueAsString();

if (name.StartsWith("r", StringComparison.InvariantCultureIgnoreCase) && color1 == default) {
color1 = arg.Value;
space = ColorHelpers.ColorSpace.RGB;
space = SharedOperations.ColorSpace.RGB;
} else if (name.StartsWith("g", StringComparison.InvariantCultureIgnoreCase) && color2 == default) {
color2 = arg.Value;
space = ColorHelpers.ColorSpace.RGB;
space = SharedOperations.ColorSpace.RGB;
} else if (name.StartsWith("b", StringComparison.InvariantCultureIgnoreCase) && color3 == default) {
color3 = arg.Value;
space = ColorHelpers.ColorSpace.RGB;
space = SharedOperations.ColorSpace.RGB;
} else if (name.StartsWith("h", StringComparison.InvariantCultureIgnoreCase) && color1 == default) {
color1 = arg.Value;
space = ColorHelpers.ColorSpace.HSV;
} else if (name.StartsWith("s", StringComparison.InvariantCultureIgnoreCase) && color2 == default) {
space = SharedOperations.ColorSpace.HSV;
} else if (name != "space" && name.StartsWith("s", StringComparison.InvariantCultureIgnoreCase) && color2 == default) {
color2 = arg.Value;
space = ColorHelpers.ColorSpace.HSV;
space = SharedOperations.ColorSpace.HSV;
} else if (name.StartsWith("v", StringComparison.InvariantCultureIgnoreCase) && color3 == default) {
color3 = arg.Value;
space = ColorHelpers.ColorSpace.HSV;
space = SharedOperations.ColorSpace.HSV;
} else if (name.StartsWith("l", StringComparison.InvariantCultureIgnoreCase) && color3 == default) {
color3 = arg.Value;
space = ColorHelpers.ColorSpace.HSL;
space = SharedOperations.ColorSpace.HSL;
} else if (name.StartsWith("a", StringComparison.InvariantCultureIgnoreCase) && a == default)
a = arg.Value;
else if (name == "space" && space == default)
space = (ColorHelpers.ColorSpace)(int)arg.Value.UnsafeGetValueAsFloat();
space = (SharedOperations.ColorSpace)(int)arg.Value.UnsafeGetValueAsFloat();
else
throw new Exception($"Invalid or double arg \"{name}\"");
}
}

if (color1 == default)
throw new Exception("Missing first component");
if (color2 == default)
throw new Exception("Missing second color component");
if (color3 == default)
throw new Exception("Missing third color component");
} else {
state.Push(DreamValue.Null);
return ProcStatus.Continue;
}

float color1Value = color1.UnsafeGetValueAsFloat();
float color2Value = color2.UnsafeGetValueAsFloat();
float color3Value = color3.UnsafeGetValueAsFloat();
byte aValue = a.IsNull ? (byte)255 : (byte)Math.Clamp((int)a.UnsafeGetValueAsFloat(), 0, 255);
Color color;
values[0] = (null, color1.UnsafeGetValueAsFloat());
values[1] = (null, color2.UnsafeGetValueAsFloat());
values[2] = (null, color3.UnsafeGetValueAsFloat());
if(a.TryGetValueAsFloat(out var aVal))
values[3] = (null, aVal);
else
values[3] = (null, null);
values[4] = (null, (float)space);

switch (space) {
case ColorHelpers.ColorSpace.RGB: {
byte r = (byte)Math.Clamp(color1Value, 0, 255);
byte g = (byte)Math.Clamp(color2Value, 0, 255);
byte b = (byte)Math.Clamp(color3Value, 0, 255);

color = new Color(r, g, b, aValue);
break;
result = SharedOperations.ParseRgb(values);
}
case ColorHelpers.ColorSpace.HSV: {
// TODO: Going beyond the max defined in the docs returns a different value. Don't know why.
float h = Math.Clamp(color1Value, 0, 360) / 360f;
float s = Math.Clamp(color2Value, 0, 100) / 100f;
float v = Math.Clamp(color3Value, 0, 100) / 100f;

color = Color.FromHsv((h, s, v, aValue / 255f));
break;
}
case ColorHelpers.ColorSpace.HSL: {
float h = Math.Clamp(color1Value, 0, 360) / 360f;
float s = Math.Clamp(color2Value, 0, 100) / 100f;
float l = Math.Clamp(color3Value, 0, 100) / 100f;

color = Color.FromHsl((h, s, l, aValue / 255f));
break;
}
default:
throw new Exception($"Unimplemented color space {space}");
}

// TODO: There is a difference between passing null and not passing a fourth arg at all
if (a.IsNull) {
state.Push(new DreamValue($"#{color.RByte:X2}{color.GByte:X2}{color.BByte:X2}".ToLower()));
} else {
state.Push(new DreamValue($"#{color.RByte:X2}{color.GByte:X2}{color.BByte:X2}{color.AByte:X2}".ToLower()));
result = "#000000";
}

state.Push(new DreamValue(result));
return ProcStatus.Continue;
}

Expand Down
Loading
Loading