Compare commits

...

4 Commits

13 changed files with 476 additions and 101 deletions

View File

@ -72,7 +72,10 @@ if(AZUR_GRAPHICS_GINT_CG)
src/gint/shaders/image_p4_dye.c
# Triangle shader
src/gint/shaders/triangle.c
src/gint/shaders/triangle.S)
src/gint/shaders/triangle.S
# Rectangle shader
src/gint/shaders/rect.c
src/gint/shaders/rect.S)
endif()
add_library(azur STATIC ${SOURCES})

View File

@ -44,6 +44,10 @@ AZUR_BEGIN_DECLS
* [fragment] is a pointer to azrp_frag. */
typedef void azrp_shader_t(void *uniforms, void *command, void *fragment);
/* azrp_shader_configure_t: Type of shader configuration functions
This function is mainly called when fragment settings change. */
typedef void azrp_shader_configure_t(void);
/* Video memory fragment used as rendering target (in XRAM). */
extern uint16_t *azrp_frag;
@ -169,6 +173,21 @@ void azrp_config_scale(int scale);
@offset Fragment offset along the y-axis (0 ... height of fragment-1). */
void azrp_config_frag_offset(int offset);
/* azrp_config_get_line(): Split a line number into fragment/offset
Sets *fragment to the first fragment that covers line y and *offset to the
line number within that fragment. */
void azrp_config_get_line(int y, int *fragment, int *offset);
/* azrp_config_get_lines(): Split a line interval into fragments and offset
Splits the interval [y; y+height) into fragment/offset pairs.
- Sets *first_fragment to the fragment that covers line y;
- Sets *first_offset to the line number within that fragment;
- Sets *fragment_count to the number of fragments the interval will cover. */
void azrp_config_get_lines(int y, int height, int *first_fragment,
int *first_offset, int *fragment_count);
//---
// Hooks
//---
@ -184,38 +203,41 @@ void azrp_hook_set_prefrag(azrp_hook_prefrag_t *);
//---
// Standard shaders
//
// None of the functions below acturally draw to the display; they all queue
// commands that get executed when azrp_render_fragment() or azrp_update() is
// called. They all return rather quickly and the time they take executing is
// counted towards command generation, not rendering.
//---
/* Clears the entire output with a single color */
extern uint8_t AZRP_SHADER_CLEAR;
/* Renders gint images with various dynamic effects */
extern uint8_t AZRP_SHADER_IMAGE_RGB16;
extern uint8_t AZRP_SHADER_IMAGE_P8;
extern uint8_t AZRP_SHADER_IMAGE_P4;
/* azrp_clear(): Clear output [ARZP_SHADER_CLEAR] */
/* azrp_clear(): Clear output with a flat color */
void azrp_clear(uint16_t color);
/* azrp_image(): Queue image command [AZRP_SHADER_IMAGE_*] */
/* azrp_image(): Render a full image, like dimage(). */
void azrp_image(int x, int y, bopti_image_t const *image);
/* azrp_subimage(): Queue image subsection command [AZRP_SHADER_IMAGE_*] */
/* azrp_subimage(): Render a section of an image, like dsubimage(). */
void azrp_subimage(int x, int y, bopti_image_t const *image,
int left, int top, int width, int height, int flags);
void azrp_triangle(int x1, int y1, int x2, int y2, int x3, int y3, int color);
/* See below for more detailed image functions. Dynamic effects are provided
with the same naming convention as gint. */
/* Functions to update uniforms for these shaders. You should call them when:
* AZRP_SHADER_CLEAR: Changing super-scaling settings.
* AZRP_SHADER_IMAGE_*: Changing super-scaling or or fragment offsets. */
void azrp_shader_clear_configure(void);
void azrp_shader_image_rgb16_configure(void);
void azrp_shader_image_p8_configure(void);
void azrp_shader_image_p4_configure(void);
void azrp_shader_triangle_configure(void);
/* azrp_triangle(): Render a flat triangle. Points can be in any order. */
void azrp_triangle(int x1, int y1, int x2, int y2, int x3, int y3, int color);
/* azrp_rect(): Render a rectangle with a flat color or color transform. */
void azrp_rect(int x1, int y1, int width, int height, int color_or_effect);
/* Effects for azrp_rect(). */
enum {
/* Invert colors in gamma space. */
AZRP_RECT_INVERT = -1,
/* Darken by halving all components in gamma space. */
AZRP_RECT_DARKEN = -2,
/* Whiten by halving the distance to white in gamma space. */
AZRP_RECT_WHITEN = -3,
};
//---
// Performance indicators
@ -251,13 +273,20 @@ void azrp_perf_clear(void);
/* azrp_register_shader(): Register a new command type and its shader program
This function adds the specified shader program to the program array, and
returns the corresponding command type. Adding new shaders is useful for
specialized rendering options (eg. tiles with fixed size) or new graphical
effects.
This function registers a new shader program to the program array, along
with its configuration function. The configuration function is called
immediately upon registration.
If the maximum number shaders is exceeded, returns -1. */
int azrp_register_shader(azrp_shader_t *program);
There is often a choice between creating a new shader and generalizing an
existing one. The impact is small; the difference only really matters if
there is a lot of commands, but in that case command management becomes a
stronger bottleneck. The choice should be made for optimal code structure
and reuse.
Returns the shader ID to be set in commands, or -1 if the maximum number of
shaders has been exceeded. */
int azrp_register_shader(azrp_shader_t *program,
azrp_shader_configure_t *configure);
/* azrp_set_uniforms(): Set a shader's uniforms pointer
@ -273,6 +302,7 @@ void azrp_set_uniforms(int shader_id, void *uniforms);
data can be updated between fragments by the shader program. Returns true on
success, false if the maximum amount of commands or command memory is
exceeded. */
// TODO: azrp_queue_command: give access to command buffer in-place
bool azrp_queue_command(void *command, size_t size, int fragment, int count);
/* azrp_queue_image(): Split and queue a gint image command

View File

@ -32,9 +32,19 @@ static uint32_t commands_array[AZRP_MAX_COMMANDS];
static GALIGNED(4) uint8_t commands_data[16384];
/* Array of shader programs and uniforms. */
static azrp_shader_t *shaders[AZRP_MAX_SHADERS] = { NULL };
static void *shader_uniforms[AZRP_MAX_SHADERS] = { NULL };
/* Shader program information. */
typedef struct {
/* Rendering function. */
azrp_shader_t *shader;
/* Uniform parameter. */
void *uniform;
/* Configuration function (in response to scale, base offset, etc). */
azrp_shader_configure_t *configure;
} shader_info_t;
/* Array of shader programs. */
static shader_info_t shaders[AZRP_MAX_SHADERS] = { 0 };
/* Next free index in the shader program array. */
static uint16_t shaders_next = 0;
@ -116,9 +126,12 @@ void azrp_render_fragments(void)
while(cmd < next_frag_threshold && i < commands_count) {
azrp_commands_total++;
uint8_t *data = commands_data + (cmd & 0xffff);
shader_info_t const *info = &shaders[data[0]];
prof_enter_norec(azrp_perf_shaders);
shaders[data[0]](shader_uniforms[data[0]], data, azrp_frag);
info->shader(info->uniform, data, azrp_frag);
prof_leave_norec(azrp_perf_shaders);
cmd = commands_array[++i];
}
@ -152,6 +165,14 @@ void azrp_update(void)
// Configuration calls
//---
static void reconfigure_all_shaders(void)
{
for(int i = 0; i < shaders_next; i++) {
if(shaders[i].configure)
shaders[i].configure();
}
}
// TODO: Use larger fragments in upscales x2 and x3
static void update_frag_count(void)
@ -184,6 +205,7 @@ void azrp_config_scale(int scale)
azrp_scale = scale;
update_size();
update_frag_count();
reconfigure_all_shaders();
}
void azrp_config_frag_offset(int offset)
@ -193,14 +215,36 @@ void azrp_config_frag_offset(int offset)
azrp_frag_offset = offset;
update_frag_count();
reconfigure_all_shaders();
}
__attribute__((constructor))
/* Make sure this constructor runs before every shader's registration
constructor so we don't configure registered shaders before the settings are
initialized. */
__attribute__((constructor(101)))
static void default_settings(void)
{
azrp_config_scale(1);
}
void azrp_config_get_line(int y, int *fragment, int *offset)
{
y += azrp_frag_offset;
*fragment = y >> 4;
*offset = y & 15;
}
void azrp_config_get_lines(int y, int height, int *first_fragment,
int *first_offset, int *fragment_count)
{
y += azrp_frag_offset;
*first_fragment = (y >> 4);
*first_offset = (y & 15);
*fragment_count = ((y + height - 1) >> 4) - *first_fragment + 1;
}
//---
// Hooks
//---
@ -219,14 +263,19 @@ void azrp_hook_set_prefrag(azrp_hook_prefrag_t *hook)
// Custom shaders
//---
int azrp_register_shader(azrp_shader_t *program)
int azrp_register_shader(azrp_shader_t *program,
azrp_shader_configure_t *configure)
{
int id = shaders_next;
if(id >= AZRP_MAX_SHADERS)
return -1;
shaders[shaders_next++] = program;
shader_info_t *info = &shaders[id];
info->shader = program;
info->uniform = NULL;
info->configure = configure;
shaders_next++;
return id;
}
@ -234,10 +283,7 @@ void azrp_set_uniforms(int shader_id, void *uniforms)
{
if((unsigned int)shader_id >= AZRP_MAX_SHADERS)
return;
if(shaders[shader_id] == NULL)
return;
shader_uniforms[shader_id] = uniforms;
shaders[shader_id].uniform = uniforms;
}
bool azrp_queue_command(void *command, size_t size, int fragment, int count)

View File

@ -2,17 +2,18 @@
uint8_t AZRP_SHADER_CLEAR = -1;
static void configure(void)
{
int longs_in_fragment = (azrp_width * azrp_frag_height / 2);
azrp_set_uniforms(AZRP_SHADER_CLEAR, (void *)longs_in_fragment);
}
__attribute__((constructor))
static void register_shader(void)
{
extern azrp_shader_t azrp_shader_clear;
AZRP_SHADER_CLEAR = azrp_register_shader(azrp_shader_clear);
}
void azrp_shader_clear_configure(void)
{
int longs_in_fragment = (azrp_width * azrp_frag_height / 2);
azrp_set_uniforms(AZRP_SHADER_CLEAR, (void *)longs_in_fragment);
AZRP_SHADER_CLEAR = azrp_register_shader(azrp_shader_clear, configure);
configure();
}
//---

View File

@ -1,6 +1,10 @@
#include <azur/gint/render.h>
#include <gint/defs/util.h>
extern uint8_t AZRP_SHADER_IMAGE_RGB16;
extern uint8_t AZRP_SHADER_IMAGE_P8;
extern uint8_t AZRP_SHADER_IMAGE_P4;
void azrp_queue_image(struct gint_image_box *box, image_t const *img,
struct gint_image_cmd *cmd)
{
@ -13,12 +17,10 @@ void azrp_queue_image(struct gint_image_box *box, image_t const *img,
else
cmd->shader_id = AZRP_SHADER_IMAGE_P4;
/* This divides by azrp_frag_height */
/* TODO: Have a proper way to do optimized-division by azrp_frag_height */
int fragment_id = (azrp_scale == 1) ? (box->y >> 4) : (box->y >> 4);
int fragment_id, first_y;
azrp_config_get_line(box->y, &fragment_id, &first_y);
/* These settings only apply to the first fragment */
int first_y = (box->y + azrp_frag_offset) & (azrp_frag_height - 1);
cmd->lines = min(box->h, azrp_frag_height - first_y);
cmd->output = (void *)azrp_frag + (azrp_width * first_y + cmd->x) * 2;

View File

@ -12,15 +12,16 @@ static void shader_p4(void *uniforms, void *command, void *fragment)
cmd->output = fragment + cmd->x * 2;
}
static void configure(void)
{
azrp_set_uniforms(AZRP_SHADER_IMAGE_P4, (void *)azrp_width);
}
__attribute__((constructor))
static void register_shader(void)
{
AZRP_SHADER_IMAGE_P4 = azrp_register_shader(shader_p4);
}
void azrp_shader_image_p4_configure(void)
{
azrp_set_uniforms(AZRP_SHADER_IMAGE_P4, (void *)azrp_width);
AZRP_SHADER_IMAGE_P4 = azrp_register_shader(shader_p4, configure);
configure();
}
void azrp_image_p4(int x, int y, image_t const *img, int eff)

View File

@ -12,15 +12,16 @@ static void shader_p8(void *uniforms, void *command, void *fragment)
cmd->output = fragment + cmd->x * 2;
}
static void configure(void)
{
azrp_set_uniforms(AZRP_SHADER_IMAGE_P8, (void *)azrp_width);
}
__attribute__((constructor))
static void register_shader(void)
{
AZRP_SHADER_IMAGE_P8 = azrp_register_shader(shader_p8);
}
void azrp_shader_image_p8_configure(void)
{
azrp_set_uniforms(AZRP_SHADER_IMAGE_P8, (void *)azrp_width);
AZRP_SHADER_IMAGE_P8 = azrp_register_shader(shader_p8, configure);
configure();
}
void azrp_image_p8(int x, int y, image_t const *img, int eff)

View File

@ -12,15 +12,16 @@ static void shader_rgb16(void *uniforms, void *command, void *fragment)
cmd->output = fragment + cmd->x * 2;
}
static void configure(void)
{
azrp_set_uniforms(AZRP_SHADER_IMAGE_RGB16, (void *)azrp_width);
}
__attribute__((constructor))
static void register_shader(void)
{
AZRP_SHADER_IMAGE_RGB16 = azrp_register_shader(shader_rgb16);
}
void azrp_shader_image_rgb16_configure(void)
{
azrp_set_uniforms(AZRP_SHADER_IMAGE_RGB16, (void *)azrp_width);
AZRP_SHADER_IMAGE_RGB16 = azrp_register_shader(shader_rgb16, configure);
configure();
}
void azrp_image_rgb16(int x, int y, image_t const *img, int eff)

View File

@ -10,7 +10,7 @@
In the simple case where there is no color effect and no HFLIP, the task of
rendering a 16-bit opaque image boils down to a 2-dimensional memcpy. This
task can be optimized by moving longwords if the source and destination and
task can be optimized by moving longwords if the source and destination are
co-4-aligned, with four variations depending on the width and initial
position, identified by the following parameters:

View File

@ -0,0 +1,185 @@
.global _azrp_shader_rect
.global _azrp_shader_rect_loop_flat
.global _azrp_shader_rect_loop_invert
.global _azrp_shader_rect_loop_darken
.global _azrp_shader_rect_loop_whiten
.align 4
#define _height r1
#define _edge_1 r2
#define _edge_2 r3
#define _stride r4
#define _cmd r5
#define _frag r6
#define _wl r7
#define _redstride r10
/* r0: (temporary)
r1: height counter
r2: (temporary) then fragment + edge_1
r3: (temporary) then fragment + edge_2
r4: stride (azrp_width * 2)
r5: cmd then color (can also be a temporary)
r6: fragment
r7: longwords to write on each line (wl)
r8: (temporary) then saved edge_1
r9: (temporary) then saved edge_2
r10: reduced stride (azrp_width * 2 - 4 * wl) */
_azrp_shader_rect:
mov.w @_cmd+, r3 /* shader_id || y */
shll _stride
mov.l r8, @-r15
nop
mov.b @_cmd+, r8 /* height_total */
extu.b r3, r3
mov.b @_cmd+, _height /* height_frag */
mulu.w r3, _stride
mov.w @_cmd+, r2 /* xl */
mov #-4, r0
sub _height, r8
mov.b r8, @(r0, _cmd) /* update: height_total */
mov.l .azrp_frag_height, r0 /* ... inefficient ... */
shll2 r2
sts macl, r3
add r2, _frag
mov.l @r0, r0
mov #0, r2
mov.w @_cmd+, _wl /* wl */
add r3, _frag
mov.l r9, @-r15
cmp/hs r0, r8
/* Next fragment height is currently r8 = remaining height
Set it to r0 = azrp_frag_height if r8 >= r0 */
bf 1f
mov r0, r8
1: mov #-5, r0
mov.b r8, @(r0, _cmd) /* update: height_frag */
mov #-7, r0
mov.b r2, @(r0, _cmd) /* update: y */
mov.w @_cmd+, _edge_1 /* edge_1 */
mov _wl, r8
mov.w @_cmd+, _edge_2 /* edge_2 */
shll2 r8
mov.l @_cmd+, r0 /* loop */
add _frag, _edge_1
mov.w @_cmd, r5 /* color */
add _frag, _edge_2
mov.l r10, @-r15
mov _stride, _redstride
jmp @r0
sub r8, _redstride
.azrp_frag_height:
.long _azrp_frag_height
.macro START
ldrs 2f
ldre 3f
1: ldrc _wl
nop
mov.w @_edge_1, r8
nop
mov.w @_edge_2, r9
nop
.endm
.macro END
dt _height
mov.w r8, @_edge_1
add _redstride, _frag
mov.w r9, @_edge_2
add _stride, _edge_1
nop
bf.s 1b
add _stride, _edge_2
mov.l @r15+, r10
mov.l @r15+, r9
rts
mov.l @r15+, r8
.endm
_azrp_shader_rect_loop_flat:
extu.w r5, r0
shll16 r5
or r0, r5
lds r5, x0
START
mov _frag, r5
2:3: movs.l x0, @r5+
mov r5, _frag
END
_azrp_shader_rect_loop_invert:
/* Inefficient: could go all the way down to 2 cycles/long with
pipelining, but we're stuck at 3 cycles/long with this naive
approach */
// TODO: time it we might be able to just non-pipeline and read ahead
// also this _frag update is suspicious
START
2: mov.l @_frag, r0
not r0, r0
mov.l r0, @_frag
3: add #4, _frag
END
nop
_azrp_shader_rect_loop_darken:
mov.l .darken_mask, r5
nop
/* Inefficient */
START
2: mov.l @_frag, r0
and r5, r0
shlr r0
mov.l r0, @_frag
3: add #4, _frag
nop
END
_azrp_shader_rect_loop_whiten:
mov.l .darken_mask, r5
nop
/* Inefficient */
START
2: mov.l @_frag, r0
not r0, r0
and r5, r0
shlr r0
not r0, r0
mov.l r0, @_frag
3: add #4, _frag
nop
END
.darken_mask:
.long 0xf7def7de

View File

@ -0,0 +1,91 @@
#include <azur/gint/render.h>
uint AZRP_SHADER_RECT = -1;
static void configure(void)
{
azrp_set_uniforms(AZRP_SHADER_RECT, (void *)azrp_width);
}
__attribute__((constructor))
static void register_shader(void)
{
extern azrp_shader_t azrp_shader_rect;
AZRP_SHADER_RECT = azrp_register_shader(azrp_shader_rect, configure);
configure();
}
//---
struct command {
uint8_t shader_id;
/* Local y coordinate of the first line in the fragment */
uint8_t y;
/* Number of lines to render total, including this fragment */
uint8_t height_total;
/* Number of lines to render on the current fragment */
uint8_t height_frag;
/* Rectangle along the x coordinates (in longwords) */
uint16_t xl, wl;
/* Offset of left edge */
int16_t edge_1;
/* Offset of right edge */
int16_t edge_2;
/* Core loop (this is an internal label of the renderer) */
void const *loop;
/* Color, when applicable */
uint16_t color;
};
/* Core loops */
extern void azrp_shader_rect_loop_flat(void);
extern void azrp_shader_rect_loop_invert(void);
extern void azrp_shader_rect_loop_darken(void);
extern void azrp_shader_rect_loop_whiten(void);
static void (*loops[])(void) = {
azrp_shader_rect_loop_flat,
azrp_shader_rect_loop_invert,
azrp_shader_rect_loop_darken,
azrp_shader_rect_loop_whiten,
};
void azrp_rect(int x1, int y1, int width0, int height0, int color_or_effect)
{
/* Clipping (x2 and y2 excluded) */
int x2 = x1 + width0;
int y2 = y1 + height0;
if(x1 < 0)
x1 = 0;
if(y1 < 0)
y1 = 0;
if(x2 > azrp_width)
x2 = azrp_width;
if(y2 > azrp_height)
y2 = azrp_height;
if(x2 <= x1 || y2 <= y1)
return;
prof_enter(azrp_perf_cmdgen);
int frag_first, first_offset, frag_count;
azrp_config_get_lines(y1, y2 - y1,
&frag_first, &first_offset, &frag_count);
struct command cmd;
cmd.shader_id = AZRP_SHADER_RECT;
cmd.y = first_offset;
cmd.height_total = y2 - y1;
cmd.height_frag = azrp_frag_height - first_offset;
if(cmd.height_total < cmd.height_frag)
cmd.height_frag = cmd.height_total;
cmd.xl = (x1 >> 1);
cmd.wl = ((x2 - 1) >> 1) - cmd.xl + 1;
cmd.edge_1 = (x1 & 1) ? 0 : -2;
cmd.edge_2 = 4 * cmd.wl + ((x2 & 1) ? -2 : 0);
cmd.loop = loops[color_or_effect >= 0 ? 0 : -color_or_effect];
cmd.color = color_or_effect;
azrp_queue_command(&cmd, sizeof cmd, frag_first, frag_count);
prof_leave(azrp_perf_cmdgen);
}

View File

@ -2,16 +2,18 @@
uint8_t AZRP_SHADER_TRIANGLE = -1;
static void configure(void)
{
azrp_set_uniforms(AZRP_SHADER_TRIANGLE, (void *)azrp_width);
}
__attribute__((constructor))
static void register_shader(void)
{
extern azrp_shader_t azrp_shader_triangle;
AZRP_SHADER_TRIANGLE = azrp_register_shader(azrp_shader_triangle);
}
void azrp_shader_triangle_configure(void)
{
azrp_set_uniforms(AZRP_SHADER_TRIANGLE, (void *)azrp_width);
AZRP_SHADER_TRIANGLE = azrp_register_shader(azrp_shader_triangle,
configure);
configure();
}
static int min(int x, int y)
@ -29,7 +31,7 @@ struct command {
uint8_t shader_id;
/* Local y coordinate of the first line in the fragment */
uint8_t y;
/* Numebr of lines to render total, including this fragment */
/* Number of lines to render total, including this fragment */
uint8_t height_total;
/* Number of lines to render on the current fragment */
uint8_t height_frag;
@ -43,7 +45,7 @@ struct command {
int u0, v0, w0;
/* Variation of each coordinate for a movement in x */
int du_x, dv_x, dw_x;
/* Variation of each coordinate for a movement in y while canceling rows's
/* Variation of each coordinate for a movement in y while canceling rows'
movements in x */
int du_row, dv_row, dw_row;
};
@ -67,12 +69,9 @@ void azrp_triangle(int x1, int y1, int x2, int y2, int x3, int y3, int color)
return;
}
/* TODO: Have a proper way to do optimized-division by azrp_frag_height
TODO: Also account for first-fragment offset */
int frag_first = min_y >> 4;
int frag_last = max_y >> 4;
int frag_count = frag_last - frag_first + 1;
int first_offset = min_y & 15;
int frag_first, first_offset, frag_count;
azrp_config_get_lines(min_y, max_y - min_y + 1,
&frag_first, &first_offset, &frag_count);
struct command cmd;
cmd.shader_id = AZRP_SHADER_TRIANGLE;
@ -83,6 +82,16 @@ void azrp_triangle(int x1, int y1, int x2, int y2, int x3, int y3, int color)
cmd.x_max = max_x;
cmd.color = color;
/* Swap points 1 and 2 if the order of points is not left-handed */
if(edge_start(x1, y1, x2, y2, x3, y3) < 0) {
int xt = x1;
x1 = x2;
x2 = xt;
int yt = y1;
y1 = y2;
y2 = yt;
}
/* Vector products for barycentric coordinates */
cmd.u0 = edge_start(x2, y2, x3, y3, min_x, min_y);
cmd.du_x = y3 - y2;

View File

@ -396,24 +396,6 @@ struct num32
int strToBuffer(char *str);
};
/* Arithmetic with integers */
inline constexpr num32 operator*(int n, num32 x) {
num32 r;
r.v = n * x.v;
return r;
}
inline constexpr num32 operator*(num32 x, int n) {
num32 r;
r.v = n * x.v;
return r;
}
inline constexpr num32 operator/(num32 x, int n) {
num32 r;
r.v = x.v / n;
return r;
}
/* num64: Signed 32:32 fixed-point type
* Size: 64 bits (8 bytes)
* Range: -2147483648.0 ... 2147483647.999999998
@ -616,6 +598,29 @@ inline constexpr T clamp(T const &val, T const &lower, T const &upper)
return max(lower, min(val, upper));
}
/* Arithmetic with integers */
template<typename T> requires(is_num<T>)
inline constexpr T operator*(int n, T const &x) {
T r;
r.v = n * x.v;
return r;
}
template<typename T> requires(is_num<T>)
inline constexpr T operator*(T const &x, int n) {
T r;
r.v = n * x.v;
return r;
}
template<typename T> requires(is_num<T>)
inline constexpr T operator/(T const &x, int n) {
T r;
r.v = x.v / n;
return r;
}
/* Other specific operations */
inline constexpr num32 num16::dmul(num16 const &x, num16 const &y)