Skip to content

Commit 87f79ca

Browse files
ike709ike709wixoaGit
authored
Compiletime rgb() evaluation (#2115)
Co-authored-by: ike709 <ike709@github.com> Co-authored-by: wixoaGit <wixoag@gmail.com>
1 parent 3b91f83 commit 87f79ca

File tree

8 files changed

+250
-84
lines changed

8 files changed

+250
-84
lines changed

Content.Tests/DMProject/Tests/Builtins/rgb.dm

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,9 @@
99
ASSERT(rgb(291.70734, 63.07692, 61.764706, space=COLORSPACE_HSL) == "#ca60db" )
1010

1111
ASSERT(rgb(291.70734, 63.07692, 61.764706, 128, COLORSPACE_HSL) == "#ca60db80" )
12-
//ASSERT(rgb(291.70734, 68.2215, 55.423534, space=COLORSPACE_HCY) == "#ca60db") // TODO Support HCY
12+
//ASSERT(rgb(291.70734, 68.2215, 55.423534, space=COLORSPACE_HCY) == "#ca60db") // TODO Support HCY
13+
14+
ASSERT(rgb(r=1, g=2, b=3) == "#010203")
15+
ASSERT(rgb(b=3, g=2, r=1) == "#010203")
16+
ASSERT(rgb(radical=1, goblin=2, baddies=3) == "#010203")
17+
ASSERT(rgb(r=1, 2, 3) == "#010203")
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
var/const/ConstProc1_a = rgb(0,0,255)
2+
3+
/proc/RunTest()
4+
var/const/ConstProc1_b = rgb(0,0,255)
5+
ASSERT(ConstProc1_a == "#0000ff")
6+
ASSERT(ConstProc1_b == "#0000ff")

DMCompiler/DM/Expressions/Builtins.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,53 @@ public override void EmitPushValue(ExpressionContext ctx) {
188188

189189
ctx.Proc.Rgb(argInfo.Type, argInfo.StackSize);
190190
}
191+
192+
// 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)
193+
public override bool TryAsConstant(DMCompiler compiler, [NotNullWhen(true)] out Constant? constant) {
194+
(string?, float?)[] values = new (string?, float?)[arguments.Length];
195+
196+
bool validArgs = true;
197+
198+
if (arguments.Length < 3 || arguments.Length > 5) {
199+
compiler.Emit(WarningCode.BadExpression, Location, $"rgb: expected 3 to 5 arguments (found {arguments.Length})");
200+
constant = null;
201+
return false;
202+
}
203+
204+
for (var index = 0; index < arguments.Expressions.Length; index++) {
205+
var (name, expr) = arguments.Expressions[index];
206+
if (!expr.TryAsConstant(compiler, out var constExpr)) {
207+
constant = null;
208+
return false;
209+
}
210+
211+
if (constExpr is not Number num) {
212+
validArgs = false;
213+
values[index] = (name, null);
214+
continue;
215+
}
216+
217+
values[index] = (name, num.Value);
218+
}
219+
220+
if (!validArgs) {
221+
compiler.Emit(WarningCode.FallbackBuiltinArgument, Location,
222+
"Non-numerical rgb argument(s) will always return \"00\"");
223+
}
224+
225+
string result;
226+
try {
227+
result = SharedOperations.ParseRgb(values);
228+
} catch (Exception e) {
229+
compiler.Emit(WarningCode.BadExpression, Location, e.Message);
230+
constant = null;
231+
return false;
232+
}
233+
234+
constant = new String(Location, result);
235+
236+
return true;
237+
}
191238
}
192239

193240
// pick(prob(50);x, prob(200);y)

DMCompiler/DMCompiler.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
<ItemGroup>
1313
<DMStandard Include="DMStandard\**" />
1414
</ItemGroup>
15+
16+
<ItemGroup>
17+
<ProjectReference Include="..\RobustToolbox\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
18+
</ItemGroup>
1519

1620
<Target Name="CopyDMStandard" AfterTargets="AfterBuild">
1721
<Copy SourceFiles="@(DMStandard)" DestinationFiles="@(DMStandard->'$(OutDir)\DMStandard\%(RecursiveDir)%(Filename)%(Extension)')" />

DMCompiler/Optimizer/CompactorOptimizations.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,4 +431,45 @@ public void Apply(DMCompiler compiler, List<IAnnotatedBytecode> input, int index
431431
}
432432
}
433433

434+
// PushNFloats [count] [float] ... [float]
435+
// Rgb [argType] [count]
436+
// -> PushString [result]
437+
// Only works when [argType] is FromStack and the [count] of both opcodes matches
438+
internal sealed class EvalRgb : IOptimization {
439+
public OptPass OptimizationPass => OptPass.ListCompactor;
440+
441+
public ReadOnlySpan<DreamProcOpcode> GetOpcodes() {
442+
return [
443+
DreamProcOpcode.PushNFloats,
444+
DreamProcOpcode.Rgb
445+
];
446+
}
447+
448+
public bool CheckPreconditions(List<IAnnotatedBytecode> input, int index) {
449+
var floatCount = ((AnnotatedBytecodeInstruction)input[index]).GetArg<AnnotatedBytecodeInteger>(0).Value;
450+
var rgbInst = (AnnotatedBytecodeInstruction)input[index + 1];
451+
var argType = rgbInst.GetArg<AnnotatedBytecodeArgumentType>(0).Value;
452+
var stackDelta = rgbInst.GetArg<AnnotatedBytecodeStackDelta>(1).Delta;
453+
454+
return argType == DMCallArgumentsType.FromStack && floatCount == stackDelta;
455+
}
456+
457+
public void Apply(DMCompiler compiler, List<IAnnotatedBytecode> input, int index) {
458+
var floats = (AnnotatedBytecodeInstruction)(input[index]);
459+
var floatArgs = floats.GetArgs();
460+
(string?, float?)[] values = new (string?, float?)[floatArgs.Count - 1];
461+
for (int i = 1; i < floatArgs.Count; i++) { // skip the first value since it's the [count] of floats
462+
values[i - 1] = (null, ((AnnotatedBytecodeFloat)floatArgs[i]).Value);
463+
}
464+
465+
var resultStr = SharedOperations.ParseRgb(values);
466+
var resultId = compiler.DMObjectTree.AddString(resultStr);
467+
468+
List<IAnnotatedBytecode> args = [new AnnotatedBytecodeString(resultId, floats.Location)];
469+
470+
input.RemoveRange(index, 2);
471+
input.Insert(index, new AnnotatedBytecodeInstruction(DreamProcOpcode.PushString, 1, args));
472+
}
473+
}
474+
434475
#endregion

DMCompiler/SharedOperations.cs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Runtime.CompilerServices;
2+
using Robust.Shared.Maths;
23

34
namespace DMCompiler;
45

@@ -73,4 +74,108 @@ public static int BitShiftLeft(int left, int right) {
7374
public static int BitShiftRight(int left, int right) {
7475
return (left & 0x00FFFFFF) >> (right) ;
7576
}
77+
78+
public enum ColorSpace {
79+
RGB = 0,
80+
HSV = 1,
81+
HSL = 2
82+
}
83+
84+
public static string ParseRgb((string? Name, float? Value)[] arguments) {
85+
string result;
86+
float? color1 = null;
87+
float? color2 = null;
88+
float? color3 = null;
89+
float? a = null;
90+
ColorSpace space = ColorSpace.RGB;
91+
92+
if (arguments[0].Name is null) {
93+
if (arguments.Length is < 3 or > 5)
94+
throw new Exception("Expected 3 to 5 arguments for rgb()");
95+
96+
color1 = arguments[0].Value;
97+
color2 = arguments[1].Value;
98+
color3 = arguments[2].Value;
99+
a = (arguments.Length >= 4) ? arguments[3].Value : null;
100+
if (arguments.Length == 5)
101+
space = arguments[4].Value is null ? ColorSpace.RGB : (ColorSpace)(int)arguments[4].Value!;
102+
} else {
103+
foreach (var arg in arguments) {
104+
var name = arg.Name ?? string.Empty;
105+
106+
if (name.StartsWith("r", StringComparison.InvariantCultureIgnoreCase) && color1 is null) {
107+
color1 = arg.Value;
108+
space = ColorSpace.RGB;
109+
} else if (name.StartsWith("g", StringComparison.InvariantCultureIgnoreCase) && color2 is null) {
110+
color2 = arg.Value;
111+
space = ColorSpace.RGB;
112+
} else if (name.StartsWith("b", StringComparison.InvariantCultureIgnoreCase) && color3 is null) {
113+
color3 = arg.Value;
114+
space = ColorSpace.RGB;
115+
} else if (name.StartsWith("h", StringComparison.InvariantCultureIgnoreCase) && color1 is null) {
116+
color1 = arg.Value;
117+
space = ColorSpace.HSV;
118+
} else if (name.StartsWith("s", StringComparison.InvariantCultureIgnoreCase) && color2 is null) {
119+
color2 = arg.Value;
120+
space = ColorSpace.HSV;
121+
} else if (name.StartsWith("v", StringComparison.InvariantCultureIgnoreCase) && color3 is null) {
122+
color3 = arg.Value;
123+
space = ColorSpace.HSV;
124+
} else if (name.StartsWith("l", StringComparison.InvariantCultureIgnoreCase) && color3 is null) {
125+
color3 = arg.Value;
126+
space = ColorSpace.HSL;
127+
} else if (name.StartsWith("a", StringComparison.InvariantCultureIgnoreCase) && a is null)
128+
a = arg.Value;
129+
else if (name == "space" && space == default)
130+
space = (ColorSpace)(int)arg.Value!;
131+
else
132+
throw new Exception($"Invalid or double arg \"{name}\"");
133+
}
134+
}
135+
136+
color1 ??= 0;
137+
color2 ??= 0;
138+
color3 ??= 0;
139+
byte aValue = a is null ? (byte)255 : (byte)Math.Clamp((int)a, 0, 255);
140+
Color color;
141+
142+
switch (space) {
143+
case ColorSpace.RGB: {
144+
byte r = (byte)Math.Clamp(color1.Value, 0, 255);
145+
byte g = (byte)Math.Clamp(color2.Value, 0, 255);
146+
byte b = (byte)Math.Clamp(color3.Value, 0, 255);
147+
148+
color = new Color(r, g, b, aValue);
149+
break;
150+
}
151+
case ColorSpace.HSV: {
152+
// TODO: Going beyond the max defined in the docs returns a different value. Don't know why.
153+
float h = Math.Clamp(color1.Value, 0, 360) / 360f;
154+
float s = Math.Clamp(color2.Value, 0, 100) / 100f;
155+
float v = Math.Clamp(color3.Value, 0, 100) / 100f;
156+
157+
color = Color.FromHsv(new(h, s, v, aValue / 255f));
158+
break;
159+
}
160+
case ColorSpace.HSL: {
161+
float h = Math.Clamp(color1.Value, 0, 360) / 360f;
162+
float s = Math.Clamp(color2.Value, 0, 100) / 100f;
163+
float l = Math.Clamp(color3.Value, 0, 100) / 100f;
164+
165+
color = Color.FromHsl(new(h, s, l, aValue / 255f));
166+
break;
167+
}
168+
default:
169+
throw new Exception($"Unimplemented color space {space}");
170+
}
171+
172+
// TODO: There is a difference between passing null and not passing a fourth arg at all
173+
if (a is null) {
174+
result = $"#{color.RByte:X2}{color.GByte:X2}{color.BByte:X2}".ToLower();
175+
} else {
176+
result = $"#{color.RByte:X2}{color.GByte:X2}{color.BByte:X2}{color.AByte:X2}".ToLower();
177+
}
178+
179+
return result;
180+
}
76181
}

0 commit comments

Comments
 (0)