ref: 98a6dffbca71cf33225e07014c1c1793e45c8e00
parent: 889302a9e227be63beb434e8d57c430d496b2511
author: Rangi <[email protected]>
date: Sun Sep 4 14:47:32 EDT 2022
Implement `opt Q` for fixed-point precision, and `q` literals (e.g. `12.34q8`) (#958) Fixes #957 Co-authored-by: ISSOtm <[email protected]>
--- a/contrib/bash_compl/_rgbasm.bash
+++ b/contrib/bash_compl/_rgbasm.bash
@@ -39,6 +39,7 @@
[M]="dependfile:glob-*.mk *.d"
[o]="output:glob-*.o"
[p]="pad-value:unk"
+ [Q]="q-precision:unk"
[r]="recursion-depth:unk"
[W]="warning:warning"
)
--- a/contrib/zsh_compl/_rgbasm
+++ b/contrib/zsh_compl/_rgbasm
@@ -56,6 +56,7 @@
'*'-MQ"+[Add a target to the rules]:target:_files -g '*.{d,mk,o}'"
'(-o --output)'{-o,--output}'+[Output file]:output file:_files'
'(-p --pad-value)'{-p,--pad-value}'+[Set padding byte]:padding byte:'
+ '(-Q --q-precision)'{-Q,--q-precision}'+[Set fixed-point precision]:precision:'
'(-r --recursion-depth)'{-r,--recursion-depth}'+[Set maximum recursion depth]:depth:'
'(-W --warning)'{-W,--warning}'+[Toggle warning flags]:warning flag:_rgbasm_warnings'
--- a/include/asm/fixpoint.h
+++ b/include/asm/fixpoint.h
@@ -11,6 +11,9 @@
#include <stdint.h>
+extern uint8_t fixPrecision;
+
+double fix_PrecisionFactor(void);
void fix_Print(int32_t i);
int32_t fix_Sin(int32_t i);
int32_t fix_Cos(int32_t i);
--- a/include/asm/opt.h
+++ b/include/asm/opt.h
@@ -14,7 +14,8 @@
void opt_B(char const chars[2]);
void opt_G(char const chars[4]);
-void opt_P(uint8_t fill);
+void opt_P(uint8_t padByte);
+void opt_Q(uint8_t precision);
void opt_L(bool optimize);
void opt_W(char const *flag);
void opt_Parse(char const *option);
--- a/man/rgbasm.1
+++ b/man/rgbasm.1
@@ -25,6 +25,7 @@
.Op Fl MQ Ar target_file
.Op Fl o Ar out_file
.Op Fl p Ar pad_value
+.Op Fl Q Ar fix_precision
.Op Fl r Ar recursion_depth
.Op Fl W Ar warning
.Ar
@@ -148,6 +149,13 @@
.It Fl p Ar pad_value , Fl Fl pad-value Ar pad_value
When padding an image, pad with this value.
The default is 0x00.
+.It Fl Q Ar fix_precision , Fl Fl q-precision Ar fix_precision
+Use this as the precision of fixed-point numbers after the decimal point, unless they specify their own precision.
+The default is 16, so fixed-point numbers are Q16.16 (since they are 32-bit integers).
+The argument may start with a
+.Ql \&.
+to match the Q notation, for example,
+.Ql Fl Q Ar .16 .
.It Fl r Ar recursion_depth , Fl Fl recursion-depth Ar recursion_depth
Specifies the recursion depth past which RGBASM will assume being in an infinite loop.
The default is 64.
--- a/man/rgbasm.5
+++ b/man/rgbasm.5
@@ -208,13 +208,14 @@
The instructions in the macro-language generally require constant expressions.
.Ss Numeric formats
There are a number of numeric formats.
-.Bl -column -offset indent "Fixed point (Q16.16)" "Prefix"
+.Bl -column -offset indent "Precise fixed-point" "Prefix"
.It Sy Format type Ta Sy Prefix Ta Sy Accepted characters
.It Hexadecimal Ta $ Ta 0123456789ABCDEF
.It Decimal Ta none Ta 0123456789
.It Octal Ta & Ta 01234567
.It Binary Ta % Ta 01
-.It Fixed point (Q16.16) Ta none Ta 01234.56789
+.It Fixed-point Ta none Ta 01234.56789
+.It Precise fixed-point Ta none Ta 12.34q8
.It Character constant Ta none Ta \(dqABYZ\(dq
.It Gameboy graphics Ta \` Ta 0123
.El
@@ -301,9 +302,19 @@
.Ic \&!
returns 1 if the operand was 0, and 0 otherwise.
.Ss Fixed-point expressions
-Fixed-point numbers are basically normal (32-bit) integers, which count 65536ths instead of entire units, offering better precision than integers but limiting the range of values.
-The upper 16 bits are used for the integer part and the lower 16 bits are used for the fraction (65536ths).
-Since they are still akin to integers, you can use them in normal integer expressions, and some integer operators like
+Fixed-point numbers are basically normal (32-bit) integers, which count fractions instead of whole numbers.
+They offer better precision than integers but limit the range of values.
+By default, the upper 16 bits are used for the integer part and the lower 16 bits are used for the fraction (65536ths).
+The default number of fractional bits can be changed with the
+.Fl Q
+command-line option.
+You can also specify a precise fixed-point value by appending a
+.Dq q
+to it followed by the number of fractional bits, such as
+.Ql 12.34q8 .
+.Pp
+Since fixed-point values are still just integers, you can use them in normal integer expressions.
+Some integer operators like
.Sq +
and
.Sq -
@@ -317,9 +328,9 @@
.EN
.Bl -column -offset indent "ATAN2(x, y)"
.It Sy Name Ta Sy Operation
-.It Fn DIV x y Ta $x \[di] y$
-.It Fn MUL x y Ta $x \[mu] y$
-.It Fn FMOD x y Ta $x % y$
+.It Fn DIV x y Ta Fixed-point division $( x \[di] y ) \[mu] ( 2 ^ precision )$
+.It Fn MUL x y Ta Fixed-point multiplication $( x \[mu] y ) \[di] ( 2 ^ precision )$
+.It Fn FMOD x y Ta Fixed-point modulo $( x % y ) \[di] ( 2 ^ precision )$
.It Fn POW x y Ta $x$ to the $y$ power
.It Fn LOG x y Ta Logarithm of $x$ to the base $y$
.It Fn ROUND x Ta Round $x$ to the nearest integer
@@ -2034,7 +2045,7 @@
The options that
.Ic OPT
can modify are currently:
-.Cm b , g , p , r , h , L ,
+.Cm b , g , p , Q , r , h , L ,
and
.Cm W .
The Boolean flag options
--- a/src/asm/fixpoint.c
+++ b/src/asm/fixpoint.c
@@ -17,17 +17,24 @@
#include "asm/symbol.h"
#include "asm/warning.h"
-#define fix2double(i) ((double)((i) / 65536.0))
-#define double2fix(d) ((int32_t)round((d) * 65536.0))
-
-// pi radians == 32768 fixed-point "degrees"
-#define fdeg2rad(f) ((f) * (M_PI / 32768.0))
-#define rad2fdeg(r) ((r) * (32768.0 / M_PI))
-
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
+#define fix2double(i) ((double)((i) / fix_PrecisionFactor()))
+#define double2fix(d) ((int32_t)round((d) * fix_PrecisionFactor()))
+
+// pi*2 radians == 2**fixPrecision fixed-point "degrees"
+#define fdeg2rad(f) ((f) * (M_PI * 2) / fix_PrecisionFactor())
+#define rad2fdeg(r) ((r) * fix_PrecisionFactor() / (M_PI * 2))
+
+uint8_t fixPrecision;
+
+double fix_PrecisionFactor(void)
+{
+ return pow(2.0, fixPrecision);
+}
+
void fix_Print(int32_t i)
{
uint32_t u = i;
@@ -38,7 +45,7 @@
sign = "-";
}
- printf("%s%" PRIu32 ".%05" PRIu32, sign, u >> 16,
+ printf("%s%" PRIu32 ".%05" PRIu32, sign, u >> fixPrecision,
((uint32_t)(fix2double(u) * 100000 + 0.5)) % 100000);
}
--- a/src/asm/format.c
+++ b/src/asm/format.c
@@ -15,6 +15,7 @@
#include <stdlib.h>
#include <string.h>
+#include "asm/fixpoint.h"
#include "asm/format.h"
#include "asm/warning.h"
@@ -225,7 +226,7 @@
} else if (fmt->type == 'f') {
// Special case for fixed-point
- // Default fractional width (C's is 6 for "%f"; here 5 is enough)
+ // Default fractional width (C's is 6 for "%f"; here 5 is enough for Q16.16)
size_t fracWidth = fmt->hasFrac ? fmt->fracWidth : 5;
if (fracWidth > 255) {
@@ -234,7 +235,8 @@
fracWidth = 255;
}
- snprintf(valueBuf, sizeof(valueBuf), "%.*f", (int)fracWidth, value / 65536.0);
+ snprintf(valueBuf, sizeof(valueBuf), "%.*f", (int)fracWidth,
+ value / fix_PrecisionFactor());
} else {
char const *spec = fmt->type == 'd' ? "%" PRId32
: fmt->type == 'u' ? "%" PRIu32
--- a/src/asm/lexer.c
+++ b/src/asm/lexer.c
@@ -27,6 +27,7 @@
#include "platform.h" // For `ssize_t`
#include "asm/lexer.h"
+#include "asm/fixpoint.h"
#include "asm/format.h"
#include "asm/fstack.h"
#include "asm/macro.h"
@@ -1125,39 +1126,66 @@
return value;
}
-static uint32_t readFractionalPart(int32_t integer)
+static uint32_t readFractionalPart(uint32_t integer)
{
uint32_t value = 0, divisor = 1;
+ uint8_t precision = 0;
+ enum {
+ READFRACTIONALPART_DIGITS,
+ READFRACTIONALPART_PRECISION,
+ READFRACTIONALPART_PRECISION_DIGITS,
+ } state = READFRACTIONALPART_DIGITS;
for (;; shiftChar()) {
int c = peek();
- if (c == '_')
- continue;
- else if (c < '0' || c > '9')
- break;
- if (divisor > (UINT32_MAX - (c - '0')) / 10) {
- warning(WARNING_LARGE_CONSTANT,
- "Precision of fixed-point constant is too large\n");
- // Discard any additional digits
- shiftChar();
- while (c = peek(), (c >= '0' && c <= '9') || c == '_')
+ if (state == READFRACTIONALPART_DIGITS) {
+ if (c == '_') {
+ continue;
+ } else if (c == 'q' || c == 'Q') {
+ state = READFRACTIONALPART_PRECISION;
+ continue;
+ } else if (c < '0' || c > '9') {
+ break;
+ }
+ if (divisor > (UINT32_MAX - (c - '0')) / 10) {
+ warning(WARNING_LARGE_CONSTANT,
+ "Precision of fixed-point constant is too large\n");
+ // Discard any additional digits
shiftChar();
- break;
+ while (c = peek(), (c >= '0' && c <= '9') || c == '_')
+ shiftChar();
+ break;
+ }
+ value = value * 10 + (c - '0');
+ divisor *= 10;
+ } else {
+ if (c == '.' && state == READFRACTIONALPART_PRECISION) {
+ state = READFRACTIONALPART_PRECISION_DIGITS;
+ continue;
+ } else if (c < '0' || c > '9') {
+ break;
+ }
+ precision = precision * 10 + (c - '0');
}
- value = value * 10 + (c - '0');
- divisor *= 10;
}
- if (integer > INT16_MAX || integer < INT16_MIN)
+ if (precision == 0) {
+ if (state >= READFRACTIONALPART_PRECISION)
+ error("Invalid fixed-point constant, no significant digits after 'q'\n");
+ precision = fixPrecision;
+ } else if (precision > 31) {
+ error("Fixed-point constant precision must be between 1 and 31\n");
+ precision = fixPrecision;
+ }
+
+ if (integer >= ((uint32_t)1 << (precision - 1)))
warning(WARNING_LARGE_CONSTANT, "Magnitude of fixed-point constant is too large\n");
- // Cast to unsigned avoids UB if shifting discards bits
- integer = (uint32_t)integer << 16;
// Cast to unsigned avoids undefined overflow behavior
- uint16_t fractional = (uint16_t)round(value * 65536.0 / divisor);
+ uint32_t fractional = (uint32_t)round((double)value / divisor * pow(2.0, precision));
- return (uint32_t)integer | (fractional * (integer >= 0 ? 1 : -1));
+ return (integer << precision) | fractional;
}
char binDigits[2];
@@ -1741,6 +1769,8 @@
static int yylex_NORMAL(void)
{
+ uint32_t num = 0;
+
for (;;) {
int c = nextChar();
char secondChar;
@@ -1912,10 +1942,12 @@
case '7':
case '8':
case '9':
- yylval.constValue = readNumber(10, c - '0');
+ num = readNumber(10, c - '0');
if (peek() == '.') {
shiftChar();
- yylval.constValue = readFractionalPart(yylval.constValue);
+ yylval.constValue = readFractionalPart(num);
+ } else {
+ yylval.constValue = num;
}
return T_NUMBER;
--- a/src/asm/main.c
+++ b/src/asm/main.c
@@ -19,6 +19,7 @@
#include <time.h>
#include "asm/charmap.h"
+#include "asm/fixpoint.h"
#include "asm/format.h"
#include "asm/fstack.h"
#include "asm/lexer.h"
@@ -86,7 +87,7 @@
}
// Short options
-static const char *optstring = "b:D:Eg:Hhi:LlM:o:p:r:VvW:w";
+static const char *optstring = "b:D:Eg:Hhi:LlM:o:p:Q:r:VvW:w";
// Variables for the long-only options
static int depType; // Variants of `-M`
@@ -116,6 +117,7 @@
{ "MQ", required_argument, &depType, 'Q' },
{ "output", required_argument, NULL, 'o' },
{ "pad-value", required_argument, NULL, 'p' },
+ { "q-precision", required_argument, NULL, 'Q' },
{ "recursion-depth", required_argument, NULL, 'r' },
{ "version", no_argument, NULL, 'V' },
{ "verbose", no_argument, NULL, 'v' },
@@ -128,7 +130,8 @@
fputs(
"Usage: rgbasm [-EHhLlVvw] [-b chars] [-D name[=value]] [-g chars] [-i path]\n"
" [-M depend_file] [-MG] [-MP] [-MT target_file] [-MQ target_file]\n"
-" [-o out_file] [-p pad_value] [-r depth] [-W warning] <file>\n"
+" [-o out_file] [-p pad_value] [-Q precision] [-r depth]\n"
+" [-W warning] <file>\n"
"Useful options:\n"
" -E, --export-all export all labels\n"
" -M, --dependfile <path> set the output dependency file\n"
@@ -170,6 +173,7 @@
opt_B("01");
opt_G("0123");
opt_P(0);
+ opt_Q(16);
haltnop = true;
warnOnHaltNop = true;
optimizeLoads = true;
@@ -250,17 +254,34 @@
out_SetFileName(musl_optarg);
break;
- unsigned long fill;
+ unsigned long padByte;
case 'p':
- fill = strtoul(musl_optarg, &ep, 0);
+ padByte = strtoul(musl_optarg, &ep, 0);
if (musl_optarg[0] == '\0' || *ep != '\0')
errx("Invalid argument for option 'p'");
- if (fill > 0xFF)
+ if (padByte > 0xFF)
errx("Argument for option 'p' must be between 0 and 0xFF");
- opt_P(fill);
+ opt_P(padByte);
+ break;
+
+ unsigned long precision;
+ const char *precisionArg;
+ case 'Q':
+ precisionArg = musl_optarg;
+ if (precisionArg[0] == '.')
+ precisionArg++;
+ precision = strtoul(precisionArg, &ep, 0);
+
+ if (musl_optarg[0] == '\0' || *ep != '\0')
+ errx("Invalid argument for option 'Q'");
+
+ if (precision < 1 || precision > 31)
+ errx("Argument for option 'Q' must be between 1 and 31");
+
+ opt_Q(precision);
break;
case 'r':
--- a/src/asm/opt.c
+++ b/src/asm/opt.c
@@ -14,6 +14,7 @@
#include <stdlib.h>
#include <string.h>
+#include "asm/fixpoint.h"
#include "asm/fstack.h"
#include "asm/lexer.h"
#include "asm/main.h"
@@ -23,7 +24,8 @@
struct OptStackEntry {
char binary[2];
char gbgfx[4];
- int32_t fillByte;
+ uint8_t fixPrecision;
+ uint8_t fillByte;
bool haltnop;
bool warnOnHaltNop;
bool optimizeLoads;
@@ -47,11 +49,16 @@
lexer_SetGfxDigits(chars);
}
-void opt_P(uint8_t fill)
+void opt_P(uint8_t padByte)
{
- fillByte = fill;
+ fillByte = padByte;
}
+void opt_Q(uint8_t precision)
+{
+ fixPrecision = precision;
+}
+
void opt_R(size_t newDepth)
{
fstk_NewRecursionDepth(newDepth);
@@ -103,18 +110,41 @@
case 'p':
if (strlen(&s[1]) <= 2) {
int result;
- unsigned int fillchar;
+ unsigned int padByte;
- result = sscanf(&s[1], "%x", &fillchar);
- if (result != EOF && result != 1)
+ result = sscanf(&s[1], "%x", &padByte);
+ if (result != 1)
error("Invalid argument for option 'p'\n");
+ else if (padByte > 0xFF)
+ error("Argument for option 'p' must be between 0 and 0xFF\n");
else
- opt_P(fillchar);
+ opt_P(padByte);
} else {
error("Invalid argument for option 'p'\n");
}
break;
+ const char *precisionArg;
+ case 'Q':
+ precisionArg = &s[1];
+ if (precisionArg[0] == '.')
+ precisionArg++;
+ if (strlen(precisionArg) <= 2) {
+ int result;
+ unsigned int precision;
+
+ result = sscanf(precisionArg, "%u", &precision);
+ if (result != 1)
+ error("Invalid argument for option 'Q'\n");
+ else if (precision < 1 || precision > 31)
+ error("Argument for option 'Q' must be between 1 and 31\n");
+ else
+ opt_Q(precision);
+ } else {
+ error("Invalid argument for option 'Q'\n");
+ }
+ break;
+
case 'r': {
++s; // Skip 'r'
while (isblank(*s))
@@ -231,6 +261,8 @@
entry->gbgfx[2] = gfxDigits[2];
entry->gbgfx[3] = gfxDigits[3];
+ entry->fixPrecision = fixPrecision; // Pulled from fixpoint.h
+
entry->fillByte = fillByte; // Pulled from section.h
entry->haltnop = haltnop; // Pulled from main.h
@@ -259,6 +291,7 @@
opt_B(entry->binary);
opt_G(entry->gbgfx);
opt_P(entry->fillByte);
+ opt_Q(entry->fixPrecision);
opt_H(entry->warnOnHaltNop);
opt_h(entry->haltnop);
opt_L(entry->optimizeLoads);
--- a/test/asm/fixed-point-precision.asm
+++ b/test/asm/fixed-point-precision.asm
@@ -12,3 +12,11 @@
fr = MUL(20.0, 0.32)
println "32% of 20 = {f:fr} (~{.2f:fr}) (~~{.0f:fr})"
+
+q8 = 1.25q8
+q16 = 1.25Q16
+q24 = 1.25q.24
+ println "Q8 ${x:q8} Q16 ${x:q16} Q24 ${x:q24}"
+
+qerr = 1.25q32
+ println qerr
--- a/test/asm/fixed-point-precision.err
+++ b/test/asm/fixed-point-precision.err
@@ -1,0 +1,3 @@
+error: fixed-point-precision.asm(21):
+ Fixed-point constant precision must be between 1 and 31
+error: Assembly aborted (1 error)!
--- a/test/asm/fixed-point-precision.out
+++ b/test/asm/fixed-point-precision.out
@@ -4,3 +4,5 @@
`16.12`: 16.119995 -> $00101eb8
`6.283185`: 6.283188 -> $0006487f
32% of 20 = 6.40015 (~6.40) (~~6)
+Q8 $140 Q16 $14000 Q24 $1400000
+$14000
--- /dev/null
+++ b/test/asm/opt-Q.asm
@@ -1,0 +1,18 @@
+MACRO test
+ PUSHO
+ OPT Q\1
+ print STRFMT("Q%4s", "\1")
+ def n = 1.14159
+ println STRFMT(" -> %032b", n)
+ POPO
+ENDM
+
+ for x, 1, 32
+ if x < 16
+ test .{d:x}
+ else
+ test {d:x}
+ endc
+ endr
+ test .0 ; error
+ test 32 ; error
--- /dev/null
+++ b/test/asm/opt-Q.err
@@ -1,0 +1,7 @@
+warning: opt-Q.asm(10) -> opt-Q.asm::REPT~1(12) -> opt-Q.asm::test(5): [-Wlarge-constant]
+ Magnitude of fixed-point constant is too large
+error: opt-Q.asm(17) -> opt-Q.asm::test(3):
+ Argument for option 'Q' must be between 1 and 31
+error: opt-Q.asm(18) -> opt-Q.asm::test(3):
+ Argument for option 'Q' must be between 1 and 31
+error: Assembly aborted (2 errors)!
--- /dev/null
+++ b/test/asm/opt-Q.out
@@ -1,0 +1,33 @@
+Q .1 -> 00000000000000000000000000000010
+Q .2 -> 00000000000000000000000000000101
+Q .3 -> 00000000000000000000000000001001
+Q .4 -> 00000000000000000000000000010010
+Q .5 -> 00000000000000000000000000100101
+Q .6 -> 00000000000000000000000001001001
+Q .7 -> 00000000000000000000000010010010
+Q .8 -> 00000000000000000000000100100100
+Q .9 -> 00000000000000000000001001001000
+Q .10 -> 00000000000000000000010010010001
+Q .11 -> 00000000000000000000100100100010
+Q .12 -> 00000000000000000001001001000100
+Q .13 -> 00000000000000000010010010001000
+Q .14 -> 00000000000000000100100100010000
+Q .15 -> 00000000000000001001001000100000
+Q 16 -> 00000000000000010010010000111111
+Q 17 -> 00000000000000100100100001111110
+Q 18 -> 00000000000001001001000011111101
+Q 19 -> 00000000000010010010000111111010
+Q 20 -> 00000000000100100100001111110100
+Q 21 -> 00000000001001001000011111101000
+Q 22 -> 00000000010010010000111111010000
+Q 23 -> 00000000100100100001111110011111
+Q 24 -> 00000001001001000011111100111110
+Q 25 -> 00000010010010000111111001111100
+Q 26 -> 00000100100100001111110011111000
+Q 27 -> 00001001001000011111100111110000
+Q 28 -> 00010010010000111111001111100000
+Q 29 -> 00100100100001111110011111000000
+Q 30 -> 01001001000011111100111110000001
+Q 31 -> 10010010000111111001111100000010
+Q .0 -> 00000000000000010010010000111111
+Q 32 -> 00000000000000010010010000111111
--- a/test/asm/opt.asm
+++ b/test/asm/opt.asm
@@ -3,11 +3,13 @@
opt !h, !L ; already the default, but tests parsing "!"
pusho
- opt p42, h, L, Wno-div
+ opt p42, Q.4, h, L, Wno-div
ds 1
ld [$ff88], a
halt
println $8000_0000 / -1
+ def n = 3.14
+ println "{x:n} = {f:n}"
popo
opt H, l
@@ -16,3 +18,5 @@
ld [$ff88], a
halt
println $8000_0000 / -1
+ def n = 3.14
+ println "{x:n} = {f:n}"
--- a/test/asm/opt.err
+++ b/test/asm/opt.err
@@ -1,2 +1,2 @@
-warning: opt.asm(18): [-Wdiv]
+warning: opt.asm(20): [-Wdiv]
Division of -2147483648 by -1 yields -2147483648
--- a/test/asm/opt.out
+++ b/test/asm/opt.out
@@ -1,2 +1,4 @@
$80000000
+32 = 3.12500
$80000000
+323d7 = 3.14000