Merge branch 'cooking' of github.com:graphitemaster/gmqcc into cooking

This commit is contained in:
Dale Weiler 2014-05-25 02:56:40 -04:00
commit 05b349c72f
16 changed files with 331 additions and 66 deletions

59
ast.c
View file

@ -75,6 +75,7 @@ static void ast_binstore_delete(ast_binstore*);
static bool ast_binstore_codegen(ast_binstore*, ast_function*, bool lvalue, ir_value**); static bool ast_binstore_codegen(ast_binstore*, ast_function*, bool lvalue, ir_value**);
static void ast_binary_delete(ast_binary*); static void ast_binary_delete(ast_binary*);
static bool ast_binary_codegen(ast_binary*, ast_function*, bool lvalue, ir_value**); static bool ast_binary_codegen(ast_binary*, ast_function*, bool lvalue, ir_value**);
static bool ast_state_codegen(ast_state*, ast_function*, bool lvalue, ir_value**);
/* It must not be possible to get here. */ /* It must not be possible to get here. */
static GMQCC_NORETURN void _ast_node_destroy(ast_node *self) static GMQCC_NORETURN void _ast_node_destroy(ast_node *self)
@ -972,6 +973,26 @@ void ast_goto_set_label(ast_goto *self, ast_label *label)
self->target = label; self->target = label;
} }
ast_state* ast_state_new(lex_ctx_t ctx, ast_expression *frame, ast_expression *think)
{
ast_instantiate(ast_state, ctx, ast_state_delete);
ast_expression_init((ast_expression*)self, (ast_expression_codegen*)&ast_state_codegen);
self->framenum = frame;
self->nextthink = think;
return self;
}
void ast_state_delete(ast_state *self)
{
if (self->framenum)
ast_unref(self->framenum);
if (self->nextthink)
ast_unref(self->nextthink);
ast_expression_delete((ast_expression*)self);
mem_d(self);
}
ast_call* ast_call_new(lex_ctx_t ctx, ast_call* ast_call_new(lex_ctx_t ctx,
ast_expression *funcexpr) ast_expression *funcexpr)
{ {
@ -3347,6 +3368,44 @@ bool ast_goto_codegen(ast_goto *self, ast_function *func, bool lvalue, ir_value
return true; return true;
} }
#include <stdio.h>
bool ast_state_codegen(ast_state *self, ast_function *func, bool lvalue, ir_value **out)
{
ast_expression_codegen *cgen;
ir_value *frameval, *thinkval;
if (lvalue) {
compile_error(ast_ctx(self), "not an l-value (state operation)");
return false;
}
if (self->expression.outr) {
compile_error(ast_ctx(self), "internal error: ast_state cannot be reused!");
return false;
}
*out = NULL;
cgen = self->framenum->codegen;
if (!(*cgen)((ast_expression*)(self->framenum), func, false, &frameval))
return false;
if (!frameval)
return false;
cgen = self->nextthink->codegen;
if (!(*cgen)((ast_expression*)(self->nextthink), func, false, &thinkval))
return false;
if (!frameval)
return false;
if (!ir_block_create_state_op(func->curblock, ast_ctx(self), frameval, thinkval)) {
compile_error(ast_ctx(self), "failed to create STATE instruction");
return false;
}
self->expression.outr = (ir_value*)1;
return true;
}
bool ast_call_codegen(ast_call *self, ast_function *func, bool lvalue, ir_value **out) bool ast_call_codegen(ast_call *self, ast_function *func, bool lvalue, ir_value **out)
{ {
ast_expression_codegen *cgen; ast_expression_codegen *cgen;

17
ast.h
View file

@ -54,6 +54,7 @@ typedef struct ast_switch_s ast_switch;
typedef struct ast_label_s ast_label; typedef struct ast_label_s ast_label;
typedef struct ast_goto_s ast_goto; typedef struct ast_goto_s ast_goto;
typedef struct ast_argpipe_s ast_argpipe; typedef struct ast_argpipe_s ast_argpipe;
typedef struct ast_state_s ast_state;
enum { enum {
AST_FLAG_VARIADIC = 1 << 0, AST_FLAG_VARIADIC = 1 << 0,
@ -111,7 +112,8 @@ enum {
TYPE_ast_switch, /* 18 */ TYPE_ast_switch, /* 18 */
TYPE_ast_label, /* 19 */ TYPE_ast_label, /* 19 */
TYPE_ast_goto, /* 20 */ TYPE_ast_goto, /* 20 */
TYPE_ast_argpipe /* 21 */ TYPE_ast_argpipe, /* 21 */
TYPE_ast_state /* 22 */
}; };
#define ast_istype(x, t) ( ((ast_node*)x)->nodetype == (TYPE_##t) ) #define ast_istype(x, t) ( ((ast_node*)x)->nodetype == (TYPE_##t) )
@ -579,6 +581,19 @@ struct ast_goto_s
ast_goto* ast_goto_new(lex_ctx_t ctx, const char *name); ast_goto* ast_goto_new(lex_ctx_t ctx, const char *name);
void ast_goto_set_label(ast_goto*, ast_label*); void ast_goto_set_label(ast_goto*, ast_label*);
/* STATE node
*
* For frame/think state updates: void foo() [framenum, nextthink] {}
*/
struct ast_state_s
{
ast_expression expression;
ast_expression *framenum;
ast_expression *nextthink;
};
ast_state* ast_state_new(lex_ctx_t ctx, ast_expression *frame, ast_expression *think);
void ast_state_delete(ast_state*);
/* CALL node /* CALL node
* *
* Contains an ast_expression as target, rather than an ast_function/value. * Contains an ast_expression as target, rather than an ast_function/value.

View file

@ -168,6 +168,11 @@ DEBUG OPTION. Print the code's intermediate representation after the
optimization and finalization passes to stdout before generating the optimization and finalization passes to stdout before generating the
binary. The instructions will be enumerated, and values will contain a binary. The instructions will be enumerated, and values will contain a
list of liferanges. list of liferanges.
.It Fl force-crc= Ns Ar CRC
Force the produced progs file to use the specified CRC.
.It Fl state-fps= Ns Ar NUM
Activate \-femulate-state and set the emulated FPS to
.Ar NUM Ns .
.El .El
.Sh COMPILE WARNINGS .Sh COMPILE WARNINGS
.Bl -tag -width Ds .Bl -tag -width Ds
@ -577,6 +582,11 @@ breaks decompilers, but causes the output file to be better compressible.
In commutative instructions, always put the lower-numbered operand first. In commutative instructions, always put the lower-numbered operand first.
This shaves off 1 byte of entropy from all these instructions, reducing This shaves off 1 byte of entropy from all these instructions, reducing
compressed size of the output file. compressed size of the output file.
.It Fl f Ns Cm emulate-state
Emulate OP_STATE operations in code rather than using the instruction.
The desired fps can be set via -state-fps=NUM, defaults to 10.
Specifying \-state-fps implicitly sets this flag. Defaults to off in all
standards.
.El .El
.Sh OPTIMIZATIONS .Sh OPTIMIZATIONS
.Bl -tag -width Ds .Bl -tag -width Ds

56
exec.c
View file

@ -55,8 +55,16 @@ qc_program_t* prog_load(const char *filename, bool skipversion)
{ {
prog_header_t header; prog_header_t header;
qc_program_t *prog; qc_program_t *prog;
size_t i;
fs_file_t *file = fs_file_open(filename, "rb"); fs_file_t *file = fs_file_open(filename, "rb");
/* we need all those in order to support INSTR_STATE: */
bool has_self = false,
has_time = false,
has_think = false,
has_nextthink = false,
has_frame = false;
if (!file) if (!file)
return NULL; return NULL;
@ -137,6 +145,36 @@ qc_program_t* prog_load(const char *filename, bool skipversion)
memset(vec_add(prog->entitydata, prog->entityfields), 0, prog->entityfields * sizeof(prog->entitydata[0])); memset(vec_add(prog->entitydata, prog->entityfields), 0, prog->entityfields * sizeof(prog->entitydata[0]));
prog->entities = 1; prog->entities = 1;
/* cache some globals and fields from names */
for (i = 0; i < vec_size(prog->defs); ++i) {
const char *name = prog_getstring(prog, prog->defs[i].name);
if (!strcmp(name, "self")) {
prog->cached_globals.self = prog->defs[i].offset;
has_self = true;
}
else if (!strcmp(name, "time")) {
prog->cached_globals.time = prog->defs[i].offset;
has_time = true;
}
}
for (i = 0; i < vec_size(prog->fields); ++i) {
const char *name = prog_getstring(prog, prog->fields[i].name);
if (!strcmp(name, "think")) {
prog->cached_fields.think = prog->fields[i].offset;
has_think = true;
}
else if (!strcmp(name, "nextthink")) {
prog->cached_fields.nextthink = prog->fields[i].offset;
has_nextthink = true;
}
else if (!strcmp(name, "frame")) {
prog->cached_fields.frame = prog->fields[i].offset;
has_frame = true;
}
}
if (has_self && has_time && has_think && has_nextthink && has_frame)
prog->supports_state = true;
return prog; return prog;
error: error:
@ -1574,8 +1612,24 @@ while (prog->vmerror == 0) {
break; break;
case INSTR_STATE: case INSTR_STATE:
qcvmerror(prog, "`%s` tried to execute a STATE operation", prog->filename); {
qcfloat_t *nextthink;
qcfloat_t *time;
qcfloat_t *frame;
if (!prog->supports_state) {
qcvmerror(prog, "`%s` tried to execute a STATE operation but misses its defs!", prog->filename);
goto cleanup;
}
ed = prog_getedict(prog, prog->globals[prog->cached_globals.self]);
((qcint_t*)ed)[prog->cached_fields.think] = OPB->function;
frame = (qcfloat_t*)&((qcint_t*)ed)[prog->cached_fields.frame];
*frame = OPA->_float;
nextthink = (qcfloat_t*)&((qcint_t*)ed)[prog->cached_fields.nextthink];
time = (qcfloat_t*)(prog->globals + prog->cached_globals.time);
*nextthink = *time + 0.1;
break; break;
}
case INSTR_GOTO: case INSTR_GOTO:
st += st->o1.s1 - 1; /* offset the s++ */ st += st->o1.s1 - 1; /* offset the s++ */

26
gmqcc.h
View file

@ -789,17 +789,17 @@ typedef struct {
} qc_exec_stack_t; } qc_exec_stack_t;
typedef struct qc_program_s { typedef struct qc_program_s {
char *filename; char *filename;
prog_section_statement_t *code; prog_section_statement_t *code;
prog_section_def_t *defs; prog_section_def_t *defs;
prog_section_def_t *fields; prog_section_def_t *fields;
prog_section_function_t *functions; prog_section_function_t *functions;
char *strings; char *strings;
qcint_t *globals; qcint_t *globals;
qcint_t *entitydata; qcint_t *entitydata;
bool *entitypool; bool *entitypool;
const char* *function_stack; const char* *function_stack;
uint16_t crc16; uint16_t crc16;
@ -825,6 +825,20 @@ typedef struct qc_program_s {
size_t xflags; size_t xflags;
int argc; /* current arg count for debugging */ int argc; /* current arg count for debugging */
/* cached fields */
struct {
qcint_t frame;
qcint_t nextthink;
qcint_t think;
} cached_fields;
struct {
qcint_t self;
qcint_t time;
} cached_globals;
bool supports_state; /* is INSTR_STATE supported? */
} qc_program_t; } qc_program_t;
qc_program_t* prog_load (const char *filename, bool ignoreversion); qc_program_t* prog_load (const char *filename, bool ignoreversion);

View file

@ -310,6 +310,11 @@
SORT_OPERANDS = false SORT_OPERANDS = false
#Emulate OP_STATE operations in code rather than using the instruction.
#The desired fps can be set via -state-fps=NUM, defaults to 10.
EMULATE_STATE = false
[warnings] [warnings]
#Generate a warning about variables which are declared but never #Generate a warning about variables which are declared but never

30
ir.c
View file

@ -1537,6 +1537,26 @@ bool ir_block_create_store_op(ir_block *self, lex_ctx_t ctx, int op, ir_value *t
return true; return true;
} }
bool ir_block_create_state_op(ir_block *self, lex_ctx_t ctx, ir_value *frame, ir_value *think)
{
ir_instr *in;
if (!ir_check_unreachable(self))
return false;
in = ir_instr_new(ctx, self, INSTR_STATE);
if (!in)
return false;
if (!ir_instr_op(in, 0, frame, false) ||
!ir_instr_op(in, 1, think, false))
{
ir_instr_delete(in);
return false;
}
vec_push(self->instr, in);
return true;
}
static bool ir_block_create_store(ir_block *self, lex_ctx_t ctx, ir_value *target, ir_value *what) static bool ir_block_create_store(ir_block *self, lex_ctx_t ctx, ir_value *target, ir_value *what)
{ {
int op = 0; int op = 0;
@ -3167,8 +3187,14 @@ static bool gen_blocks_recursive(code_t *code, ir_function *func, ir_block *bloc
} }
if (instr->opcode == INSTR_STATE) { if (instr->opcode == INSTR_STATE) {
irerror(block->context, "TODO: state instruction"); stmt.opcode = instr->opcode;
return false; if (instr->_ops[0])
stmt.o1.u1 = ir_value_code_addr(instr->_ops[0]);
if (instr->_ops[1])
stmt.o2.u1 = ir_value_code_addr(instr->_ops[1]);
stmt.o3.u1 = 0;
code_push_statement(code, &stmt, instr->context);
continue;
} }
stmt.opcode = instr->opcode; stmt.opcode = instr->opcode;

1
ir.h
View file

@ -170,6 +170,7 @@ bool GMQCC_WARN ir_block_create_store_op(ir_block*, lex_ctx_t, int op, ir_value
bool GMQCC_WARN ir_block_create_storep(ir_block*, lex_ctx_t, ir_value *target, ir_value *what); bool GMQCC_WARN ir_block_create_storep(ir_block*, lex_ctx_t, ir_value *target, ir_value *what);
ir_value* ir_block_create_load_from_ent(ir_block*, lex_ctx_t, const char *label, ir_value *ent, ir_value *field, int outype); ir_value* ir_block_create_load_from_ent(ir_block*, lex_ctx_t, const char *label, ir_value *ent, ir_value *field, int outype);
ir_value* ir_block_create_fieldaddress(ir_block*, lex_ctx_t, const char *label, ir_value *entity, ir_value *field); ir_value* ir_block_create_fieldaddress(ir_block*, lex_ctx_t, const char *label, ir_value *entity, ir_value *field);
bool GMQCC_WARN ir_block_create_state_op(ir_block*, lex_ctx_t, ir_value *frame, ir_value *think);
/* This is to create an instruction of the form /* This is to create an instruction of the form
* <outtype>%label := opcode a, b * <outtype>%label := opcode a, b

6
main.c
View file

@ -85,6 +85,7 @@ static int usage(void) {
" -Ono-<name> disable specific optimization\n" " -Ono-<name> disable specific optimization\n"
" -Ohelp list optimizations\n"); " -Ohelp list optimizations\n");
con_out(" -force-crc=num force a specific checksum into the header\n"); con_out(" -force-crc=num force a specific checksum into the header\n");
con_out(" -state-fps=num emulate OP_STATE with the specified FPS\n");
con_out(" -coverage add coverage support\n"); con_out(" -coverage add coverage support\n");
return -1; return -1;
} }
@ -217,6 +218,11 @@ static bool options_parse(int argc, char **argv) {
OPTS_OPTION_U16 (OPTION_FORCED_CRC) = strtol(argarg, NULL, 0); OPTS_OPTION_U16 (OPTION_FORCED_CRC) = strtol(argarg, NULL, 0);
continue; continue;
} }
if (options_long_gcc("state-fps", &argc, &argv, &argarg)) {
OPTS_OPTION_U32(OPTION_STATE_FPS) = strtol(argarg, NULL, 0);
opts_set(opts.flags, EMULATE_STATE, true);
continue;
}
if (options_long_gcc("redirout", &argc, &argv, &redirout)) { if (options_long_gcc("redirout", &argc, &argv, &redirout)) {
con_change(redirout, redirerr); con_change(redirout, redirerr);
continue; continue;

2
opts.c
View file

@ -101,6 +101,8 @@ static void opts_setdefault(void) {
opts_set(opts.flags, LEGACY_VECTOR_MATHS, true); opts_set(opts.flags, LEGACY_VECTOR_MATHS, true);
opts_set(opts.flags, DARKPLACES_STRING_TABLE_BUG, true); opts_set(opts.flags, DARKPLACES_STRING_TABLE_BUG, true);
/* options */
OPTS_OPTION_U32(OPTION_STATE_FPS) = 10;
} }
void opts_backup_non_Wall() { void opts_backup_non_Wall() {

View file

@ -56,6 +56,7 @@
GMQCC_DEFINE_FLAG(UNSAFE_VARARGS) GMQCC_DEFINE_FLAG(UNSAFE_VARARGS)
GMQCC_DEFINE_FLAG(TYPELESS_STORES) GMQCC_DEFINE_FLAG(TYPELESS_STORES)
GMQCC_DEFINE_FLAG(SORT_OPERANDS) GMQCC_DEFINE_FLAG(SORT_OPERANDS)
GMQCC_DEFINE_FLAG(EMULATE_STATE)
#endif #endif
/* warning flags */ /* warning flags */
@ -135,6 +136,7 @@
GMQCC_DEFINE_FLAG(STATISTICS) GMQCC_DEFINE_FLAG(STATISTICS)
GMQCC_DEFINE_FLAG(PROGSRC) GMQCC_DEFINE_FLAG(PROGSRC)
GMQCC_DEFINE_FLAG(COVERAGE) GMQCC_DEFINE_FLAG(COVERAGE)
GMQCC_DEFINE_FLAG(STATE_FPS)
#endif #endif
/* some cleanup so we don't have to */ /* some cleanup so we don't have to */

124
parser.c
View file

@ -4015,69 +4015,83 @@ static bool parse_function_body(parser_t *parser, ast_value *var)
} }
if (has_frame_think) { if (has_frame_think) {
lex_ctx_t ctx; if (!OPTS_FLAG(EMULATE_STATE)) {
ast_expression *self_frame; ast_state *state_op = ast_state_new(parser_ctx(parser), framenum, nextthink);
ast_expression *self_nextthink; if (!ast_block_add_expr(block, (ast_expression*)state_op)) {
ast_expression *self_think; parseerror(parser, "failed to generate state op for [frame,think]");
ast_expression *time_plus_1; ast_unref(nextthink);
ast_store *store_frame; ast_unref(framenum);
ast_store *store_nextthink; ast_delete(block);
ast_store *store_think; return false;
}
} else {
/* emulate OP_STATE in code: */
lex_ctx_t ctx;
ast_expression *self_frame;
ast_expression *self_nextthink;
ast_expression *self_think;
ast_expression *time_plus_1;
ast_store *store_frame;
ast_store *store_nextthink;
ast_store *store_think;
ctx = parser_ctx(parser); float frame_delta = 1.0f / (float)OPTS_OPTION_U32(OPTION_STATE_FPS);
self_frame = (ast_expression*)ast_entfield_new(ctx, gbl_self, fld_frame);
self_nextthink = (ast_expression*)ast_entfield_new(ctx, gbl_self, fld_nextthink);
self_think = (ast_expression*)ast_entfield_new(ctx, gbl_self, fld_think);
time_plus_1 = (ast_expression*)ast_binary_new(ctx, INSTR_ADD_F, ctx = parser_ctx(parser);
gbl_time, (ast_expression*)fold_constgen_float(parser->fold, 0.1f)); self_frame = (ast_expression*)ast_entfield_new(ctx, gbl_self, fld_frame);
self_nextthink = (ast_expression*)ast_entfield_new(ctx, gbl_self, fld_nextthink);
self_think = (ast_expression*)ast_entfield_new(ctx, gbl_self, fld_think);
if (!self_frame || !self_nextthink || !self_think || !time_plus_1) { time_plus_1 = (ast_expression*)ast_binary_new(ctx, INSTR_ADD_F,
if (self_frame) ast_delete(self_frame); gbl_time, (ast_expression*)fold_constgen_float(parser->fold, frame_delta));
if (self_nextthink) ast_delete(self_nextthink);
if (self_think) ast_delete(self_think);
if (time_plus_1) ast_delete(time_plus_1);
retval = false;
}
if (retval) if (!self_frame || !self_nextthink || !self_think || !time_plus_1) {
{ if (self_frame) ast_delete(self_frame);
store_frame = ast_store_new(ctx, INSTR_STOREP_F, self_frame, framenum); if (self_nextthink) ast_delete(self_nextthink);
store_nextthink = ast_store_new(ctx, INSTR_STOREP_F, self_nextthink, time_plus_1); if (self_think) ast_delete(self_think);
store_think = ast_store_new(ctx, INSTR_STOREP_FNC, self_think, nextthink); if (time_plus_1) ast_delete(time_plus_1);
if (!store_frame) {
ast_delete(self_frame);
retval = false; retval = false;
} }
if (!store_nextthink) {
ast_delete(self_nextthink); if (retval)
retval = false;
}
if (!store_think) {
ast_delete(self_think);
retval = false;
}
if (!retval) {
if (store_frame) ast_delete(store_frame);
if (store_nextthink) ast_delete(store_nextthink);
if (store_think) ast_delete(store_think);
retval = false;
}
if (!ast_block_add_expr(block, (ast_expression*)store_frame) ||
!ast_block_add_expr(block, (ast_expression*)store_nextthink) ||
!ast_block_add_expr(block, (ast_expression*)store_think))
{ {
retval = false; store_frame = ast_store_new(ctx, INSTR_STOREP_F, self_frame, framenum);
} store_nextthink = ast_store_new(ctx, INSTR_STOREP_F, self_nextthink, time_plus_1);
} store_think = ast_store_new(ctx, INSTR_STOREP_FNC, self_think, nextthink);
if (!retval) { if (!store_frame) {
parseerror(parser, "failed to generate code for [frame,think]"); ast_delete(self_frame);
ast_unref(nextthink); retval = false;
ast_unref(framenum); }
ast_delete(block); if (!store_nextthink) {
return false; ast_delete(self_nextthink);
retval = false;
}
if (!store_think) {
ast_delete(self_think);
retval = false;
}
if (!retval) {
if (store_frame) ast_delete(store_frame);
if (store_nextthink) ast_delete(store_nextthink);
if (store_think) ast_delete(store_think);
retval = false;
}
if (!ast_block_add_expr(block, (ast_expression*)store_frame) ||
!ast_block_add_expr(block, (ast_expression*)store_nextthink) ||
!ast_block_add_expr(block, (ast_expression*)store_think))
{
retval = false;
}
}
if (!retval) {
parseerror(parser, "failed to generate code for [frame,think]");
ast_unref(nextthink);
ast_unref(framenum);
ast_delete(block);
return false;
}
} }
} }

2
test.c
View file

@ -85,7 +85,7 @@ static fs_file_t **task_popen(const char *command, const char *mode) {
while (*line != '\0' && *line != ' ' && while (*line != '\0' && *line != ' ' &&
*line != '\t' && *line != '\n') line++; *line != '\t' && *line != '\n') line++;
} }
vec_push(argv, '\0'); vec_push(argv, NULL);
} }

9
tests/state-emu.tmpl Normal file
View file

@ -0,0 +1,9 @@
I: state.qc
D: test emulated state ops
T: -execute
C: -std=gmqcc -femulate-state
M: st1, .frame=1, .nextthink=10.1 (now: 10)
M: st2, .frame=2, .nextthink=11.1 (now: 11)
M: st3, .frame=0, .nextthink=12.1 (now: 12)
M: st1, .frame=1, .nextthink=13.1 (now: 13)
M: st2, .frame=2, .nextthink=14.1 (now: 14)

39
tests/state.qc Normal file
View file

@ -0,0 +1,39 @@
float time;
entity self;
.void() think;
.float nextthink;
.float frame;
void stprint(string fun) {
print(fun,
", .frame=", ftos(self.frame),
", .nextthink=", ftos(self.nextthink),
" (now: ", ftos(time), ")\n");
}
void st1() = [1, st2] { stprint("st1"); }
void st2() = [2, st3] { stprint("st2"); }
void st3() = [0, st1] { stprint("st3"); }
void main() {
entity ea = spawn();
entity eb = spawn();
time = 10;
self = ea;
self.think = st1;
self.nextthink = time;
self.frame = 100;
self.think();
time = 11;
self.think();
time = 12;
self.think();
time = 13;
self.think();
time = 14;
self.think();
};

9
tests/state.tmpl Normal file
View file

@ -0,0 +1,9 @@
I: state.qc
D: test state ops
T: -execute
C: -std=gmqcc
M: st1, .frame=1, .nextthink=10.1 (now: 10)
M: st2, .frame=2, .nextthink=11.1 (now: 11)
M: st3, .frame=0, .nextthink=12.1 (now: 12)
M: st1, .frame=1, .nextthink=13.1 (now: 13)
M: st2, .frame=2, .nextthink=14.1 (now: 14)