From 0c72525755062da49c0397bad18a8ed8c2ff75df Mon Sep 17 00:00:00 2001 From: zongor Date: Tue, 14 Oct 2025 15:27:29 -0700 Subject: [PATCH] Emscripten Paint Works!!!!!! --- Makefile | 11 +-- src/arch/emscripten/devices.c | 76 ++------------- src/arch/emscripten/devices.h | 2 + src/arch/emscripten/main.c | 175 ++++++++++++++++++++++++++++++++-- src/arch/linux/devices.c | 26 +---- src/arch/linux/devices.h | 2 + src/arch/linux/main.c | 99 ++++++++++++++++++- test/paint-bw.asm.lisp | 85 ++++++++++------- test/paint-bw.rom | Bin 683 -> 758 bytes test/paint.asm.lisp | 5 +- test/paint.rom | Bin 1144 -> 1330 bytes 11 files changed, 336 insertions(+), 145 deletions(-) diff --git a/Makefile b/Makefile index 81a03fe..569fb0b 100644 --- a/Makefile +++ b/Makefile @@ -55,16 +55,15 @@ endif # --- EMSCRIPTEN-SPECIFIC --- ifeq ($(PLATFORM), emscripten) - LDFLAGS += -s USE_SDL=2 -s WASM=1 \ + LDFLAGS += -s USE_SDL=2\ + -s ASYNCIFY \ -s ALLOW_MEMORY_GROWTH=1 \ - -s MAX_WEBGL_VERSION=2 \ - --preload-file test/paint-bw.rom@paint.rom \ - -s INITIAL_MEMORY=16MB + --preload-file test/paint.rom@paint.rom # For release: optimize, strip debug, minimize size ifeq ($(BUILD_MODE), release) - PLATFORM_CFLAGS += -O2 -flto - LDFLAGS += -O2 -flto + PLATFORM_CFLAGS += -O2 + LDFLAGS += -O2 endif # Output: HTML + JS + WASM diff --git a/src/arch/emscripten/devices.c b/src/arch/emscripten/devices.c index 844ed0e..3938612 100644 --- a/src/arch/emscripten/devices.c +++ b/src/arch/emscripten/devices.c @@ -121,30 +121,14 @@ i32 screen_write(void *data, const u8 *buffer, u32 size) { return -1; } + if (!screen->screen_buffer) { + screen->screen_buffer = (u8*)buffer; + } + // Update texture with new frame data SDL_UpdateTexture(screen->texture, NULL, buffer, screen->width); - - // Clear and render - SDL_RenderClear(screen->renderer); - - SDL_Rect output_rect; - SDL_RenderGetViewport(screen->renderer, &output_rect); - - // Calculate aspect ratio preserving scaling - float scale_x = (float)output_rect.w / screen->width; - float scale_y = (float)output_rect.h / screen->height; - float scale = SDL_min(scale_x, scale_y); - - SDL_Rect dstrect = { - (i32)((output_rect.w - screen->width * scale) / 2), - (i32)((output_rect.h - screen->height * scale) / 2), - (i32)(screen->width * scale), - (i32)(screen->height * scale) - }; - - SDL_RenderCopy(screen->renderer, screen->texture, NULL, &dstrect); - SDL_RenderPresent(screen->renderer); - + screen->update = true; + return 0; } @@ -210,54 +194,6 @@ i32 mouse_read(void *data, u8 *buffer, u32 size) { if (size < 12) return -1; - SDL_PumpEvents(); - SDL_Event event; - while (SDL_PollEvent(&event)) { - switch (event.type) { - // Mouse events - case SDL_MOUSEMOTION: - mouse_data->x = event.motion.x; - mouse_data->y = event.motion.y; - break; - - case SDL_MOUSEBUTTONDOWN: - if (event.button.button == SDL_BUTTON_LEFT) mouse_data->btn1 = 1; - if (event.button.button == SDL_BUTTON_RIGHT) mouse_data->btn2 = 1; - if (event.button.button == SDL_BUTTON_MIDDLE) mouse_data->btn3 = 1; - if (event.button.button == SDL_BUTTON_X1) mouse_data->btn4 = 1; - break; - - case SDL_MOUSEBUTTONUP: - if (event.button.button == SDL_BUTTON_LEFT) mouse_data->btn1 = 0; - if (event.button.button == SDL_BUTTON_RIGHT) mouse_data->btn2 = 0; - if (event.button.button == SDL_BUTTON_MIDDLE) mouse_data->btn3 = 0; - if (event.button.button == SDL_BUTTON_X1) mouse_data->btn4 = 0; - break; - - // Touch events (map to mouse_data as left-click equivalent) - case SDL_FINGERMOTION: - case SDL_FINGERDOWN: - case SDL_FINGERUP: { - - float x = event.tfinger.x * 640; - float y = event.tfinger.y * 480; - - mouse_data->x = (int)x; - mouse_data->y = (int)y; - - // Only treat the first finger as mouse input (ignore multi-touch beyond 1 finger) - if (event.tfinger.fingerId == 0) { - if (event.type == SDL_FINGERDOWN || event.type == SDL_FINGERMOTION) { - mouse_data->btn1 = 1; - } else if (event.type == SDL_FINGERUP) { - mouse_data->btn1 = 0; - } - } - break; - } - } - } - u8 *info = (u8 *)buffer; memcpy(&info[0], &mouse_data->x, sizeof(u32)); memcpy(&info[4], &mouse_data->y, sizeof(u32)); diff --git a/src/arch/emscripten/devices.h b/src/arch/emscripten/devices.h index 64308b3..526b37d 100644 --- a/src/arch/emscripten/devices.h +++ b/src/arch/emscripten/devices.h @@ -8,6 +8,8 @@ typedef struct screen_device_data_s { u32 height; u32 pos; u32 size; + u8 *screen_buffer; + bool update; SDL_Window *window; SDL_Renderer *renderer; SDL_Texture *texture; diff --git a/src/arch/emscripten/main.c b/src/arch/emscripten/main.c index 4f97ee9..4b93d85 100644 --- a/src/arch/emscripten/main.c +++ b/src/arch/emscripten/main.c @@ -1,5 +1,5 @@ -#include "../../vm/vm.h" #include "../../vm/device.h" +#include "../../vm/vm.h" #include "devices.h" #include #include @@ -31,10 +31,171 @@ static DeviceOps console_device_ops = { static ScreenDeviceData screen_data = {0}; static MouseDeviceData mouse_data = {0}; +const char *opcode_to_string(Opcode op) { + static const char *names[] = {[OP_HALT] = "halt", + [OP_JMP] = "jump", + [OP_JMPF] = "jump-if-flag", + [OP_CALL] = "call", + [OP_RETURN] = "return", + [OP_LOAD_IMM] = "load-immediate", + [OP_GET_8] = "get-8", + [OP_GET_16] = "get-16", + [OP_GET_32] = "get", + [OP_LOAD_8] = "load-8", + [OP_LOAD_16] = "load-16", + [OP_LOAD_32] = "load", + [OP_STORE_8] = "store-8", + [OP_STORE_16] = "store-16", + [OP_STORE_32] = "store", + [OP_PUT_8] = "put-8", + [OP_PUT_16] = "put-16", + [OP_PUT_32] = "put", + [OP_MALLOC] = "malloc", + [OP_MEMSET_8] = "memset-8", + [OP_MEMSET_16] = "memset-16", + [OP_MEMSET_32] = "memset-32", + [OP_PUSH] = "push", + [OP_POP] = "pop", + [OP_REG_MOV] = "register-move", + [OP_SYSCALL] = "syscall", + [OP_SLL] = "bit-shift-left", + [OP_SRL] = "bit-shift-right", + [OP_SRE] = "bit-shift-re", + [OP_BAND] = "bit-and", + [OP_BOR] = "bit-or", + [OP_BXOR] = "bit-xor", + [OP_ADD_INT] = "add-int", + [OP_SUB_INT] = "sub-int", + [OP_MUL_INT] = "mul-int", + [OP_DIV_INT] = "div-int", + [OP_ADD_UINT] = "add-nat", + [OP_SUB_UINT] = "sub-nat", + [OP_MUL_UINT] = "mul-nat", + [OP_DIV_UINT] = "div-nat", + [OP_ADD_REAL] = "add-real", + [OP_SUB_REAL] = "sub-real", + [OP_MUL_REAL] = "mul-real", + [OP_DIV_REAL] = "div-real", + [OP_INT_TO_REAL] = "int-to-real", + [OP_UINT_TO_REAL] = "nat-to-real", + [OP_REAL_TO_INT] = "real-to-int", + [OP_REAL_TO_UINT] = "real-to-nat", + [OP_JEQ_INT] = "jump-eq-int", + [OP_JNEQ_INT] = "jump-neq-int", + [OP_JGT_INT] = "jump-gt-int", + [OP_JLT_INT] = "jump-lt-int", + [OP_JLE_INT] = "jump-le-int", + [OP_JGE_INT] = "jump-ge-int", + [OP_JEQ_UINT] = "jump-eq-nat", + [OP_JNEQ_UINT] = "jump-neq-nat", + [OP_JGT_UINT] = "jump-gt-nat", + [OP_JLT_UINT] = "jump-lt-nat", + [OP_JLE_UINT] = "jump-le-nat", + [OP_JGE_UINT] = "jump-ge-nat", + [OP_JEQ_REAL] = "jump-eq-real", + [OP_JNEQ_REAL] = "jump-neq-real", + [OP_JGE_REAL] = "jump-ge-real", + [OP_JGT_REAL] = "jump-gt-real", + [OP_JLT_REAL] = "jump-lt-real", + [OP_JLE_REAL] = "jump-le-real", + [OP_STRLEN] = "string-length", + [OP_STREQ] = "string-eq", + [OP_STRCAT] = "string-concat", + [OP_STR_GET_CHAR] = "string-get-char", + [OP_STR_FIND_CHAR] = "string-find-char", + [OP_STR_SLICE] = "string-slice", + [OP_INT_TO_STRING] = "int-to-string", + [OP_UINT_TO_STRING] = "nat-to-string", + [OP_REAL_TO_STRING] = "real-to-string", + [OP_STRING_TO_INT] = "string-to-int", + [OP_STRING_TO_UINT] = "string-to-nat", + [OP_STRING_TO_REAL] = "string-to-real"}; + + if (op < 0 || op >= (int)(sizeof(names) / sizeof(names[0]))) { + return ""; + } + + const char *name = names[op]; + return name ? name : ""; +} + void mainloop() { - if (!step_vm(&vm)) { - emscripten_cancel_main_loop(); - printf("VM execution completed\n"); + SDL_Event event; + SDL_PumpEvents(); + while (SDL_PollEvent(&event)) { + switch (event.type) { + // Mouse events + case SDL_MOUSEMOTION: + mouse_data.x = event.motion.x; + mouse_data.y = event.motion.y; + break; + + case SDL_MOUSEBUTTONDOWN: + if (event.button.button == SDL_BUTTON_LEFT) + mouse_data.btn1 = 1; + if (event.button.button == SDL_BUTTON_RIGHT) + mouse_data.btn2 = 1; + if (event.button.button == SDL_BUTTON_MIDDLE) + mouse_data.btn3 = 1; + if (event.button.button == SDL_BUTTON_X1) + mouse_data.btn4 = 1; + break; + + case SDL_MOUSEBUTTONUP: + if (event.button.button == SDL_BUTTON_LEFT) + mouse_data.btn1 = 0; + if (event.button.button == SDL_BUTTON_RIGHT) + mouse_data.btn2 = 0; + if (event.button.button == SDL_BUTTON_MIDDLE) + mouse_data.btn3 = 0; + if (event.button.button == SDL_BUTTON_X1) + mouse_data.btn4 = 0; + break; + + // Touch events (map to mouse_data as left-click equivalent) + case SDL_FINGERMOTION: + case SDL_FINGERDOWN: + case SDL_FINGERUP: { + + float x = event.tfinger.x * 640; + float y = event.tfinger.y * 480; + + mouse_data.x = (int)x; + mouse_data.y = (int)y; + + // Only treat the first finger as mouse input (ignore multi-touch beyond 1 + // finger) + if (event.tfinger.fingerId == 0) { + if (event.type == SDL_FINGERDOWN || event.type == SDL_FINGERMOTION) { + mouse_data.btn1 = 1; + } else if (event.type == SDL_FINGERUP) { + mouse_data.btn1 = 0; + } + } + break; + } + } + } + + // Run VM for a fixed number of cycles or a time slice + int cycles_this_frame = 0; + int max_cycles_per_frame = 1000; // Adjust this value + while (cycles_this_frame < max_cycles_per_frame) { + // printf("%s\n", opcode_to_string(vm.code[vm.pc])); // REMOVE THIS LINE + if (!step_vm(&vm)) { + emscripten_cancel_main_loop(); + return; + } + cycles_this_frame++; + } + + // Render only if the screen buffer was updated AND at a reasonable rate + if (screen_data.update) { + if (screen_data.renderer && screen_data.texture) { + SDL_RenderCopy(screen_data.renderer, screen_data.texture, NULL, NULL); + SDL_RenderPresent(screen_data.renderer); + } + screen_data.update = false; // Reset flag after rendering } } @@ -84,13 +245,13 @@ int main(int argc, char **argv) { } SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0"); - #ifdef __EMSCRIPTEN__ +#ifdef __EMSCRIPTEN__ emscripten_set_canvas_element_size("#canvas", 640, 480); - #endif +#endif loadVM("paint.rom", &vm); printf("VM loaded successfully\n"); - + // Initialize device data screen_data.width = 640; screen_data.height = 480; diff --git a/src/arch/linux/devices.c b/src/arch/linux/devices.c index cf45dcb..5e704e1 100644 --- a/src/arch/linux/devices.c +++ b/src/arch/linux/devices.c @@ -89,29 +89,13 @@ i32 screen_write(void *data, const u8 *buffer, u32 size) { if (size > screen->size * sizeof(u8)) { return -1; } + if (!screen->screen_buffer) { + screen->screen_buffer = (u8*)buffer; + } + // Update texture with new frame data SDL_UpdateTexture(screen->texture, NULL, buffer, screen->width); - - // Clear and render - SDL_RenderClear(screen->renderer); - - SDL_Rect output_rect; - SDL_RenderGetViewport(screen->renderer, &output_rect); - - // Calculate aspect ratio preserving scaling - float scale_x = (float)output_rect.w / screen->width; - float scale_y = (float)output_rect.h / screen->height; - float scale = SDL_min(scale_x, scale_y); - - SDL_Rect dstrect = { - (i32)((output_rect.w - screen->width * scale) / 2), - (i32)((output_rect.h - screen->height * scale) / 2), - (i32)(screen->width * scale), - (i32)(screen->height * scale) - }; - - SDL_RenderCopy(screen->renderer, screen->texture, NULL, &dstrect); - SDL_RenderPresent(screen->renderer); + screen->update = true; return 0; } diff --git a/src/arch/linux/devices.h b/src/arch/linux/devices.h index 6c00fcb..29b1343 100644 --- a/src/arch/linux/devices.h +++ b/src/arch/linux/devices.h @@ -10,6 +10,8 @@ typedef struct screen_device_data_s { u32 height; u32 pos; u32 size; + u8 *screen_buffer; + u32 update; SDL_Window *window; SDL_Renderer *renderer; SDL_Texture *texture; diff --git a/src/arch/linux/main.c b/src/arch/linux/main.c index 1a10efd..bbe641c 100644 --- a/src/arch/linux/main.c +++ b/src/arch/linux/main.c @@ -394,8 +394,8 @@ i32 main(i32 argc, char *argv[]) { screen_data.height = 480; screen_data.size = screen_data.width * screen_data.height; - vm_register_device(&vm, "/dev/screen/0", "screen", - &screen_data, &screen_ops); + vm_register_device(&vm, "/dev/screen/0", "screen", &screen_data, + &screen_ops); mouse_data.x = 0; mouse_data.y = 0; @@ -411,12 +411,105 @@ i32 main(i32 argc, char *argv[]) { vm_register_device(&vm, "/dev/keyboard/0", "keyboard", &keyboard_data, &keyboard_ops); + SDL_Event event; bool running = true; + SDL_PumpEvents(); while (running) { - if (!step_vm(&vm)) { + while (SDL_PollEvent(&event)) { + switch (event.type) { + case SDL_QUIT: running = false; break; + // Mouse events + case SDL_MOUSEMOTION: + mouse_data.x = event.motion.x; + mouse_data.y = event.motion.y; + break; + + case SDL_MOUSEBUTTONDOWN: + if (event.button.button == SDL_BUTTON_LEFT) + mouse_data.btn1 = 1; + if (event.button.button == SDL_BUTTON_RIGHT) + mouse_data.btn2 = 1; + if (event.button.button == SDL_BUTTON_MIDDLE) + mouse_data.btn3 = 1; + if (event.button.button == SDL_BUTTON_X1) + mouse_data.btn4 = 1; + break; + + case SDL_MOUSEBUTTONUP: + if (event.button.button == SDL_BUTTON_LEFT) + mouse_data.btn1 = 0; + if (event.button.button == SDL_BUTTON_RIGHT) + mouse_data.btn2 = 0; + if (event.button.button == SDL_BUTTON_MIDDLE) + mouse_data.btn3 = 0; + if (event.button.button == SDL_BUTTON_X1) + mouse_data.btn4 = 0; + break; + + // Touch events (map to mouse_data as left-click equivalent) + case SDL_FINGERMOTION: + case SDL_FINGERDOWN: + case SDL_FINGERUP: { + + float x = event.tfinger.x * 640; + float y = event.tfinger.y * 480; + + mouse_data.x = (int)x; + mouse_data.y = (int)y; + + // Only treat the first finger as mouse input (ignore multi-touch + // beyond 1 finger) + if (event.tfinger.fingerId == 0) { + if (event.type == SDL_FINGERDOWN || + event.type == SDL_FINGERMOTION) { + mouse_data.btn1 = 1; + } else if (event.type == SDL_FINGERUP) { + mouse_data.btn1 = 0; + } + } + break; } + } + } + + // Run VM for a fixed number of cycles or a time slice + int cycles_this_frame = 0; + int max_cycles_per_frame = 1000; // Adjust this value + while (cycles_this_frame < max_cycles_per_frame) { + if (!step_vm(&vm)) { + running = false; + } + cycles_this_frame++; + } + + // Render only if the screen buffer was updated AND at a reasonable rate + if (screen_data.update) { + if (screen_data.renderer && screen_data.texture) { + // Clear and render + SDL_RenderClear(screen_data.renderer); + + SDL_Rect output_rect; + SDL_RenderGetViewport(screen_data.renderer, &output_rect); + + // Calculate aspect ratio preserving scaling + float scale_x = (float)output_rect.w / screen_data.width; + float scale_y = (float)output_rect.h / screen_data.height; + float scale = SDL_min(scale_x, scale_y); + + SDL_Rect dstrect = { + (i32)((output_rect.w - screen_data.width * scale) / 2), + (i32)((output_rect.h - screen_data.height * scale) / 2), + (i32)(screen_data.width * scale), + (i32)(screen_data.height * scale)}; + + SDL_RenderCopy(screen_data.renderer, screen_data.texture, NULL, + &dstrect); + SDL_RenderPresent(screen_data.renderer); + } + screen_data.update = false; // Reset flag after rendering + } } } else { bool running = true; diff --git a/test/paint-bw.asm.lisp b/test/paint-bw.asm.lisp index e6a247f..ae6a87b 100644 --- a/test/paint-bw.asm.lisp +++ b/test/paint-bw.asm.lisp @@ -1,52 +1,68 @@ ((code (label main - (load-immediate $30 4) ; fat ptr size - ; Open screen ; use load immediate because it is a pointer to a string, not a value (load-immediate $0 &screen-namespace) (load-immediate $11 0) - (syscall OPEN $0 $11) + (syscall OPEN $0 $0 $11) (load-immediate $16 1) ; device info call (load-immediate $17 16) ; sizeof screen device info (malloc $18 $17) (syscall IOCTL $0 $16 $18) - (load-immediate $1 12) ; offset for width (add-nat $19 $18 $1) (get $20 $19) ; load width - (load-immediate $1 8) ; offset for size (add-nat $19 $18 $1) - (get $22 $19) ; load size - + (get $22 $19) ; load size (malloc $21 $22) ; malloc frame buffer (load-immediate $16 &mouse-namespace) (load-immediate $3 12) ; malloc sizeof mouse data (malloc $4 $3) + (syscall OPEN $16 $16 $4) - (load-immediate $14 20) ; box size + (push $21) + (push $20) + (load $1 &BLACK) + (push $1) + (load-immediate $12 1) + (push $12) + (load-immediate $13 1) + (push $13) + (call &draw-outlined-swatch) + (push $21) + (push $20) + (load $1 &WHITE) + (push $1) + (load-immediate $12 21) + (push $12) + (load-immediate $13 1) + (push $13) + (call &draw-outlined-swatch) + (syscall WRITE $0 $21 $22) + (label draw-loop ; load mouse click data - (syscall READ $16 $2 $3 $4) - - (load-immediate $5 4) ; offset for x - (add-nat $6 $5 $2) - (get $7 $6) ; load x - - (load-immediate $5 8) ; offset for y - (add-nat $6 $5 $2) - (get $8 $6) ; load y - + (syscall READ $16 $2 $3 $4) (load-immediate $5 12) ; offset for btn1 (add-nat $6 $5 $2) (get-8 $9 $6) ; load btn1 pressed + (load-immediate $14 20) ; box size + + (jump-eq-nat &draw-loop $9 $11) + + (load-immediate $5 4) ; offset for x + (add-nat $6 $5 $2) + (get $7 $6) ; load x + (load-immediate $5 8) ; offset for y + (add-nat $6 $5 $2) + (get $8 $6) ; load y (load-immediate $5 13) ; offset for btn2 (add-nat $6 $5 $2) (get-8 $10 $6) ; load btn2 pressed @@ -61,8 +77,7 @@ (load-immediate $13 1) (push $13) (call &draw-outlined-swatch) - (syscall WRITE $0 $21 $22) - + (push $14) ; box_size (20) (push $13) ; box_y (push $12) ; box_x @@ -93,14 +108,17 @@ (syscall WRITE $0 $21 $22) - (jump-eq-nat &draw-loop $9 $11) - - (mul-nat $15 $8 $20) ; $15 = y * width - (add-nat $15 $15 $7) ; $15 += x - (add-nat $15 $21 $15) ; $15 = base + pixel_offset - (add-nat $15 $15 $30) (load $22 &SELECTED-COLOR) ; color - (store-8 $15 $22) ; draw color at screen [x,y] + (load-immediate $1 5) ; size of brush + + (push $21) ;base + (push $20) ;width + (push $22) ; color + (push $7) ;x + (push $8) ;y + (push $1) + (push $1) + (call &draw-box) (jump-eq-nat &draw-loop $10 $11)) @@ -208,9 +226,7 @@ (label draw-box-outer (add-int $6 $4 $12) ; $6 = row end = current + width (register-move $7 $4) ; $7 = pixel pointer - - (memset-8 $7 $3 $12) - + (memset-8 $7 $3 $12) ; draw row (add-int $4 $4 $2) ; next row (+= 640) (sub-int $5 $5 $1) ; decrement row count (jump-gt-int &draw-box-outer $5 0)) @@ -219,7 +235,8 @@ (label screen-namespace "/dev/screen/0") (label mouse-namespace "/dev/mouse/0") (label SELECTED-COLOR 255) - (label BLACK 0) - (label WHITE 255) - (label DARK-GRAY 73) - (label GRAY 146))) + (label BLACK 0) + (label WHITE 255) + (label DARK-GRAY 73) + (label GRAY 146) + (label LIGHT-GRAY 182))) diff --git a/test/paint-bw.rom b/test/paint-bw.rom index 1dc90e389dd98628418bfe56173f7cf5d134ba66..1758b2d8f2a5ef561e8e3d71d3beaca556ad5453 100644 GIT binary patch literal 758 zcma)3JB}1F5N+F4cDvjCnV#wLbk@LR1T)%|5E3B~F-KrW+GH1r6*%qHT-CAT*2B|l14Rdtb5u5*!1w%KKzy~Q~=y#67}vRCT7BI6X9a1b(` zu#*0%s(}kFH`wGc!fWQD-W+~$T4fHu*#Sg&#W#qu5cDgtK~}5EfGjyeLRB`GHq6ll zy)rC0k|(cMkXzQ!vMqTWn_!X&Yc2T$%yw6GU@$ v%X9GrEtl?Zj-U1q?>-!l_xnR3Kc~#y`;QMtCQeLTo-=;4kY5-7eer()5R9?n(l)xGl}#N}4+`OE&|v5b-m#H-ZWd`VYGFLtOc5uG^~E z4Go$_ot#t8RFqQc!LYeHZ?5jRbIhNOTsb%$I7}s8Xv;F&pa;q~+d&@xdds5<)sAtvHj=Gl}Q0FnB7ILBC*u zG@?vKh_PX`-x)7mXrf3+9R-v5)jzm9K3WUEWHiQrFkJedGgKW^2GB- zo>*EI)Eus!rD&FvMtLIMv>!_8rnppKTD-#&TnZK!=gba3ug|$JoZQ5z>$~IE_37>B en9XRJt?kUHUA|M8a8WkF)G^f;$#HPCnN4kCBO6*}jXol`UVB-z z_21f4HCIP|PTTvPsyP_>Y@<0G`CQ=qKRy*bJim+KdA8APMn2cJ@Odkm!Dk!Ihmp^% zEqp$WeEzK&KKGS-Br~zGJ4966pZvM4rSBh62PplHn!~M%W3Ne?xbYg*_>FF`3w-QC zjr)q0cs}eIW&rzBqSCO(CMtx!&sOItOH#ne4y{TiG0+}9?KNv^12<{RxS7>$S~pRv zI|Kj&2Q-7oJ&eJWuOS}*S0q^9YKshb z<)y>O{fCb?YZ^GFfs--h3mLp2d?WlQd@B4Zd@cMXG(!F_aPTST;J*vUnI7JIA-^aN lD%{0HSP5SUxkWji_wbH{&xGHE*M+ZyFNHsZUxYt}zX5SlQxO0F literal 1144 zcmZXTy^0h;5XYy#cV=p~cc*&>4jg4U6FHXk?m{s#P%#iAGY!rSPr*e^{YXp%(a=DA z2Emsw@e$;F0>Mx?t9oj1nXRCIQ`P_ePtA09mSx#e;jk44;(r3^5lTXv>LtvGFrVRA z)$`WS+^hyc+C`{^jRC}DNZL>}ZJS=vv@ol&4vM6eR;@(Nq|#tC>8+90SuklaB@;}N zEha9@ZRC`My;Co^F>=@u zoQxcRDusdpY=wU~&897`ZJA7op;IJe3I&!!-cKAMW*b)ps?Dzp{@^ar#;=+5k z@QnwNw#j>8+I^gz`!=bHGK&z)m_-p~RMB)+n$9V6bdz>i>chK4tB>v@h0=!)iB9)B zwLHZPDh)+4ElQ0Z61`$7~Bv9KLOewmB6nID+n zm@k<>neUl@nK_fL7e#+>@pAQPc7gesNk57L8&6R%1M>}&P8!AQ9@#JEE9M#V7V{nR ME%P_?2lEf}AIp?PbN~PV