From 8395e88545e307d04906331cda37d87d6bb723a4 Mon Sep 17 00:00:00 2001 From: Ty Overby Date: Sat, 5 Aug 2023 16:48:24 -0400 Subject: [PATCH 1/7] add some benchmarks and attempt to speed things up --- .gitignore | 7 ++ example/bench.ml | 107 +++++++++++++++++++++++++++ example/benchmarks.ml | 164 ++++++++++++++++++++++++++++++++++++++++++ example/dune | 2 +- lib/wall.ml | 24 ++++--- lib/wall__backend.ml | 2 + lib/wall__geom.ml | 1 + lib/wall_text.ml | 57 ++++++++++----- lib/wall_text.mli | 1 + 9 files changed, 337 insertions(+), 28 deletions(-) create mode 100644 example/bench.ml create mode 100644 example/benchmarks.ml diff --git a/.gitignore b/.gitignore index eb652c1..73c4fb0 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,13 @@ *.cmxa .merlin +# opam switch +_opam + +# perf tracing +perf.data +perf.data.* + # ocamlbuild working directory _build/ diff --git a/example/bench.ml b/example/bench.ml new file mode 100644 index 0000000..0c0dd30 --- /dev/null +++ b/example/bench.ml @@ -0,0 +1,107 @@ +open! Tsdl +open! Tgles2 + +open! Wall +module I = Image +module P = Path +module Text = Wall_text + +module type S = sig + type state + val init : Wall.Renderer.t -> state + val frame : state -> width:float -> height:float -> elapsed_seconds:float -> I.t +end + +let load_font name = + let ic = open_in_bin name in + let dim = in_channel_length ic in + let fd = Unix.descr_of_in_channel ic in + let buffer = + Unix.map_file fd Bigarray.int8_unsigned Bigarray.c_layout false [|dim|] + |> Bigarray.array1_of_genarray + in + let offset = List.hd (Stb_truetype.enum buffer) in + match Stb_truetype.init buffer offset with + | None -> assert false + | Some font -> font + +let font_sans = lazy (load_font "Roboto-Regular.ttf") + +let run (module T: S) = + let window_width = 1000 in + let window_height = 800 in + Printexc.record_backtrace true; + match Sdl.init Sdl.Init.video with + | Error (`Msg e) -> Sdl.log "Init error: %s" e; exit 1 + | Ok () -> + ignore (Sdl.gl_set_attribute Sdl.Gl.depth_size 24 : _ result); + ignore (Sdl.gl_set_attribute Sdl.Gl.stencil_size 8 : _ result); + match + Sdl.create_window ~w:window_width ~h:window_height "SDL OpenGL" + Sdl.Window.(opengl + allow_highdpi) + with + | Error (`Msg e) -> Sdl.log "Create window error: %s" e; exit 1 + | Ok w -> + (*Sdl.gl_set_attribute Sdl.Gl.context_profile_mask Sdl.Gl.context_profile_core; + Sdl.gl_set_attribute Sdl.Gl.context_major_version 2; + Sdl.gl_set_attribute Sdl.Gl.context_minor_version 1;*) + ignore (Sdl.gl_set_swap_interval (-1)); + let ow, oh = Sdl.gl_get_drawable_size w in + Sdl.log "window size: %d,%d\topengl drawable size: %d,%d" window_width window_height ow oh; + let _sw = float ow /. float window_width and _sh = float oh /. float window_height in + ignore (Sdl.gl_set_attribute Sdl.Gl.stencil_size 1); + match Sdl.gl_create_context w with + | Error (`Msg e) -> Sdl.log "Create context error: %s" e; exit 1 + | Ok ctx -> + let context = Renderer.create ~antialias:true () in + let state = T.init context in + let quit = ref false in + let event = Sdl.Event.create () in + let prev_frame_fps = ref 0.0 in + let freq = Sdl.get_performance_frequency () in + let font_sans = Lazy.force font_sans in + while not !quit do + let timing_start = Sdl.get_performance_counter () in + while Sdl.poll_event (Some event) do + match Sdl.Event.enum (Sdl.Event.get event Sdl.Event.typ) with + | `Quit -> quit := true + | _ -> () + done; + Gl.viewport 0 0 ow oh; + Gl.clear_color 0.3 0.3 0.32 1.0; + Gl.(clear (color_buffer_bit lor depth_buffer_bit lor stencil_buffer_bit)); + Gl.enable Gl.blend; + Gl.blend_func_separate Gl.one Gl.src_alpha Gl.one Gl.one_minus_src_alpha; + Gl.enable Gl.cull_face_enum; + Gl.disable Gl.depth_test; + let elapsed_seconds = (Int32.to_float (Sdl.get_ticks ()) /. 1000.0) in + let () = + let width = (float window_width) in + let height = (float window_height) in + let image = T.frame state ~width ~height ~elapsed_seconds in + let fps = + let fps = Printf.sprintf " FPS: %d" (Float.to_int !prev_frame_fps) in + I.stack + (I.paint (Paint.color (Color.v 0.0 0.0 0.0 1.0)) + Text.(simple_text + (Font.make ~blur:2.0 ~size:30.0 font_sans) + ~valign:`TOP ~halign:`LEFT + ~x:0.0 ~y:0.0 fps)) + (I.paint (Paint.color (Color.v 1.0 1.0 1.0 1.0)) + Text.(simple_text + (Font.make ~size:30.0 font_sans) + ~valign:`TOP ~halign:`LEFT + ~x:0.0 ~y:0.0 fps)) + in + Renderer.render context ~width ~height (I.stack image fps) + in + Sdl.gl_swap_window w; + let timing_end = Sdl.get_performance_counter () in + let seconds_elapsed = Int64.to_float (Int64.sub timing_end timing_start) /. (Int64.to_float freq) in + prev_frame_fps := (1.0 /. seconds_elapsed); + () + done; + Sdl.gl_delete_context ctx; + Sdl.destroy_window w; + Sdl.quit (); + exit 0 diff --git a/example/benchmarks.ml b/example/benchmarks.ml new file mode 100644 index 0000000..b4217c1 --- /dev/null +++ b/example/benchmarks.ml @@ -0,0 +1,164 @@ +open Wall +module I = Image +module P = Path +module Text = Wall_text + + +module Many_graphs: Bench.S = struct + type state = unit + let init _ctx = () + + let draw_graph x y w h t = + let samples = [| + (1.0 +. sin (t *. 1.2345 +. cos (t *. 0.33457) *. 0.44 )) *. 0.5; + (1.0 +. sin (t *. 0.68363 +. cos (t *. 1.3 ) *. 1.55 )) *. 0.5; + (1.0 +. sin (t *. 1.1642 +. cos (t *. 0.33457) *. 1.24 )) *. 0.5; + (1.0 +. sin (t *. 0.56345 +. cos (t *. 1.63 ) *. 0.14 )) *. 0.5; + (1.0 +. sin (t *. 1.6245 +. cos (t *. 0.254 ) *. 0.3 )) *. 0.5; + (1.0 +. sin (t *. 0.345 +. cos (t *. 0.03 ) *. 0.6 )) *. 0.5; + |] in + let dx = w /. 5.0 in + let sx i = x +. float i *. dx in + let sy i = y +. h *. samples.(i) *. 0.8 in + I.seq [ + (* Graph background *) + I.paint + (Paint.linear_gradient ~sx:x ~sy:y ~ex:x ~ey:(y +. h) + ~inner:(Color.v 0.00 0.60 0.75 0.00) + ~outer:(Color.v 0.00 0.60 0.75 0.25)) + (I.fill_path @@ fun t -> + P.move_to t ~x:(sx 0) ~y:(sy 0); + for i = 1 to 5 do + P.bezier_to t + ~c1x:(sx (i - 1) +. dx *. 0.5) ~c1y:(sy (i - 1)) + ~c2x:(sx i -. dx *. 0.5) ~c2y:(sy i) + ~x:(sx i) ~y:(sy i) + done; + P.line_to t ~x:(x +. w) ~y:(y +. h); + P.line_to t ~x ~y:(y +. h)); + (* Graph line *) + I.paint (Paint.color (Color.v 0.0 0.0 0.0 0.125)) + (I.stroke_path Outline.{default with stroke_width = 3.0} @@ fun t -> + P.move_to t ~x:(sx 0) ~y:(sy 0 +. 2.0); + for i = 1 to 5 do + P.bezier_to t + ~c1x:(sx (i - 1) +. dx *. 0.5) ~c1y:(sy (i - 1) +. 2.0) + ~c2x:(sx i -. dx *. 0.5) ~c2y:(sy i +. 2.0) + ~x:(sx i) ~y:(sy i +. 2.0) + done); + I.paint (Paint.color (Color.v 0.0 0.60 0.75 1.0)) + (I.stroke_path Outline.{default with stroke_width = 3.0} @@ fun t -> + P.move_to t ~x:(sx 0) ~y:(sy 0); + for i = 1 to 5 do + P.bezier_to t + ~c1x:(sx (i - 1) +. dx *. 0.5) ~c1y:(sy (i - 1)) + ~c2x:(sx i -. dx *. 0.5) ~c2y:(sy i) + ~x:(sx i) ~y:(sy i) + done); + (* Graph sample pos *) + (let node = ref I.empty in + for i = 0 to 5 do + node := I.stack !node ( + I.paint + (Paint.radial_gradient ~cx:(sx i) ~cy:(sy i +. 2.0) ~inr:3.0 ~outr:8.0 + ~inner:(Color.v 0.0 0.0 0.0 0.125) ~outer:(Color.v 0.0 0.0 0.0 0.0)) + (I.fill_path @@ fun t -> + P.rect t ~x:(sx i -. 10.0) ~y:(sy i -. 10.0 +. 2.0) ~w:20.0 ~h:20.0)) + done; + !node); + I.paint (Paint.color (Color.v 0.0 0.6 0.75 1.0)) + (I.fill_path @@ fun t -> + for i = 0 to 5 do + P.circle t ~cx:(sx i) ~cy:(sy i) ~r:4.0; + done); + I.paint (Paint.color (Color.v 0.8 0.8 0.8 1.0)) + (I.fill_path @@ fun t -> + for i = 0 to 5 do + P.circle t ~cx:(sx i) ~cy:(sy i) ~r:2.0 + done) + ] + ;; + + let many_graphs ~width:w ~height:h t = + let node = ref I.empty in + let push n = node := I.stack !node n in + for i = 0 to 500 do + push @@ draw_graph 0.0 0.0 w h (t +. (float i)); + done; + !node + ;; + + let many_graphs_cached = ref None + + let many_graphs ~width ~height time = + match !many_graphs_cached with + | Some (w, h, t, cached) + when Float.equal w width + && Float.equal h height + && Float.equal t time -> cached + | _ -> + let cached = many_graphs ~width ~height time in + many_graphs_cached := Some (width, height, time, cached); + cached + + let frame () ~width ~height ~elapsed_seconds = + I.stack + (many_graphs ~width ~height 0.0) + (draw_graph 0.0 0.0 width height elapsed_seconds) +end + +module Lots_of_text: Bench.S = struct + type command = { + color: Color.t; + text : string; + leave: bool; + x: float; + y: float } + + type state = { + font : Text.Font.t; + commands : command list + } + + let init _ctx = + let font = + let tt = Lazy.force Bench.font_sans in + (Text.Font.make ~size:30.0 ~placement:`Subpixel tt) + in + let commands = ref [] in + let push i = commands := i :: !commands in + for x = 0 to 100 do + let x = Float.of_int x *. 100.0 in + for y = 0 to 100 do + let y = Float.of_int y *. 50.0 in + let c1, c2 = + Color.v (Random.float 1.0) (Random.float 1.0) (Random.float 1.0) (Random.float 1.0), + Color.v (Random.float 1.0) (Random.float 1.0) (Random.float 1.0) (Random.float 1.0) in + push {color = c1; text = "hello"; leave = Random.bool (); x; y}; + push {color = c2; text = "world"; leave = Random.bool (); x; y}; + done; + done; + {font; commands = !commands} + + let paint_text ~x ~y ~color ~font s = + I.paint + (Paint.color color) + (Text.simple_text font ~valign:`TOP ~halign:`LEFT ~x ~y s) + + let frame {font; commands} ~width ~height ~elapsed_seconds = + let matrix = + let scale = (Transform.scale ~sx:((Float.sin elapsed_seconds +. 1.25) /. 2.0) ~sy:((Float.sin elapsed_seconds +. 1.25) /. 2.0)) in + let forward = (Transform.translation ~x:(-. width /. 2.0 -. 5000.0) ~y:(-. height /. 2.0 -. 2500.0)) in + let backward = (Transform.translation ~x:(width /. 2.0) ~y:(height /. 2.0)) in + Transform.compose forward (Transform.compose scale backward) + (* Transform.compose sc tr *) + in + let should_disappear = (Float.to_int (elapsed_seconds /. 2.0)) mod 2 = 0 in + List.filter_map (fun {color; text; leave; x; y} -> + if should_disappear && leave then None + else Some (paint_text ~x ~y ~color ~font text)) commands + |> I.seq + |> I.transform matrix +end +(* let () = Bench.run (module Many_graphs) *) +let () = Bench.run (module Lots_of_text) diff --git a/example/dune b/example/dune index eca315d..39dc48e 100644 --- a/example/dune +++ b/example/dune @@ -1,4 +1,4 @@ (executables - (names example minimal blemish colorweb) + (names example minimal blemish colorweb benchmarks) (flags :standard -w -3-6-27) (libraries tsdl tgls.tgles2 wall)) diff --git a/lib/wall.ml b/lib/wall.ml index 4fe2fbe..ae5118b 100644 --- a/lib/wall.ml +++ b/lib/wall.ml @@ -20,6 +20,8 @@ open Gg open Wall_types +let compare = () + module Backend = Wall__backend type renderer = { @@ -977,23 +979,25 @@ module Renderer = struct x0 = 0.0; y0 = 0.0; x1 = 0.0; y1 = 0.0; u0 = 0.0; v0 = 0.0; u1 = 0.0; v1 = 0.0} + let ( .%{}<-) (arr: B.bigarray) idx v = Bigarray.Array1.unsafe_set arr idx v;; + let push_quad b = let d = B.data b and c = B.alloc b (6 * 4) in let q = quadbuf in - d.{c+ 0+0}<-q.x0; d.{c+ 0+1}<-q.y0; d.{c+ 0+2}<-q.u0; d.{c+ 0+3}<-q.v0; - d.{c+ 4+0}<-q.x1; d.{c+ 4+1}<-q.y1; d.{c+ 4+2}<-q.u1; d.{c+ 4+3}<-q.v1; - d.{c+ 8+0}<-q.x1; d.{c+ 8+1}<-q.y0; d.{c+ 8+2}<-q.u1; d.{c+ 8+3}<-q.v0; - d.{c+12+0}<-q.x0; d.{c+12+1}<-q.y0; d.{c+12+2}<-q.u0; d.{c+12+3}<-q.v0; - d.{c+16+0}<-q.x0; d.{c+16+1}<-q.y1; d.{c+16+2}<-q.u0; d.{c+16+3}<-q.v1; - d.{c+20+0}<-q.x1; d.{c+20+1}<-q.y1; d.{c+20+2}<-q.u1; d.{c+20+3}<-q.v1 + d.%{c+ 0+0}<-q.x0; d.%{c+ 0+1}<-q.y0; d.%{c+ 0+2}<-q.u0; d.%{c+ 0+3}<-q.v0; + d.%{c+ 4+0}<-q.x1; d.%{c+ 4+1}<-q.y1; d.%{c+ 4+2}<-q.u1; d.%{c+ 4+3}<-q.v1; + d.%{c+ 8+0}<-q.x1; d.%{c+ 8+1}<-q.y0; d.%{c+ 8+2}<-q.u1; d.%{c+ 8+3}<-q.v0; + d.%{c+12+0}<-q.x0; d.%{c+12+1}<-q.y0; d.%{c+12+2}<-q.u0; d.%{c+12+3}<-q.v0; + d.%{c+16+0}<-q.x0; d.%{c+16+1}<-q.y1; d.%{c+16+2}<-q.u0; d.%{c+16+3}<-q.v1; + d.%{c+20+0}<-q.x1; d.%{c+20+1}<-q.y1; d.%{c+20+2}<-q.u1; d.%{c+20+3}<-q.v1 let push_quad_strip b = let d = B.data b and c = B.alloc b (4 * 4) in let q = quadbuf in - d.{c+ 0+0}<-q.x1; d.{c+ 0+1}<-q.y1; d.{c+ 0+2}<-q.u1; d.{c+ 0+3}<-q.v1; - d.{c+ 4+0}<-q.x1; d.{c+ 4+1}<-q.y0; d.{c+ 4+2}<-q.u1; d.{c+ 4+3}<-q.v0; - d.{c+ 8+0}<-q.x0; d.{c+ 8+1}<-q.y1; d.{c+ 8+2}<-q.u0; d.{c+ 8+3}<-q.v1; - d.{c+12+0}<-q.x0; d.{c+12+1}<-q.y0; d.{c+12+2}<-q.u0; d.{c+12+3}<-q.v0 + d.%{c+ 0+0}<-q.x1; d.%{c+ 0+1}<-q.y1; d.%{c+ 0+2}<-q.u1; d.%{c+ 0+3}<-q.v1; + d.%{c+ 4+0}<-q.x1; d.%{c+ 4+1}<-q.y0; d.%{c+ 4+2}<-q.u1; d.%{c+ 4+3}<-q.v0; + d.%{c+ 8+0}<-q.x0; d.%{c+ 8+1}<-q.y1; d.%{c+ 8+2}<-q.u0; d.%{c+ 8+3}<-q.v1; + d.%{c+12+0}<-q.x0; d.%{c+12+1}<-q.y0; d.%{c+12+2}<-q.u0; d.%{c+12+3}<-q.v0 let scale_factor xf = let sx = Utils.norm xf.x00 xf.x10 in diff --git a/lib/wall__backend.ml b/lib/wall__backend.ml index 379eb2a..da2d124 100644 --- a/lib/wall__backend.ml +++ b/lib/wall__backend.ml @@ -2,6 +2,8 @@ open Wall_types open Gg open Bigarray +let compare = () + type state external wall_gl_create diff --git a/lib/wall__geom.ml b/lib/wall__geom.ml index 00c58bb..068008d 100644 --- a/lib/wall__geom.ml +++ b/lib/wall__geom.ml @@ -15,6 +15,7 @@ misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. *) +let compare = () [@@@landmark "auto"] module BA = Bigarray.Array1 diff --git a/lib/wall_text.ml b/lib/wall_text.ml index f9a23fa..e6a66b1 100644 --- a/lib/wall_text.ml +++ b/lib/wall_text.ml @@ -1,5 +1,6 @@ open Wall open Wall__geom +let compare = () (* utf-8 decoding dfa, from http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ *) @@ -45,6 +46,7 @@ module Font = struct type t = { glyphes: Stb_truetype.t; + glyphes_id : int; size: float; blur: float; spacing: float; @@ -53,7 +55,8 @@ module Font = struct } let make ?(size=16.0) ?(blur=0.0) ?(spacing=0.0) ?(line_height=1.0) ?(placement=`Aligned) glyphes = - { glyphes; blur; size; spacing; line_height; placement } + let glyphes_id = Oo.id (Obj.magic glyphes) in + { glyphes; glyphes_id; blur; size; spacing; line_height; placement } type metrics = { ascent : float; @@ -148,14 +151,16 @@ module Glyph = struct cp : int; scale : int; ttf : Stb_truetype.t; + ttf_id : int; blur : int; } let key ~sx ~sy font = let ttf = font.Font.glyphes in + let ttf_id = font.Font.glyphes_id in let blur = decimal_quantize font.Font.blur in let factor, scale = estimate_scale sx sy font in - (factor, (fun cp -> { cp; scale; ttf; blur })) + (factor, (fun cp -> { cp; scale; ttf; ttf_id; blur })) type cell = { box : Stb_truetype.box; @@ -176,15 +181,33 @@ let null_cell = {Glyph. box = null_box; uv = null_box; glyph = Stb_truetype.invalid_glyph; frame = -1 } +module Glyphtbl = Hashtbl.Make (struct + type t = Glyph.key + + let equal (a :t) (b:t) = + a.cp == b.cp && + a.blur == b.blur && + a.scale == b.scale && + a.ttf_id == b.ttf_id + + let hash (a :t) = + let hash = a.cp in + let hash = Int.logxor (hash * 0x1f1f1f1f) a.blur in + let hash = Int.logxor (hash * 0x45d9f3b0) a.scale in + let hash = Int.logxor (hash * 0x119de1f3) a.ttf_id in + hash + +end) type font_stash = { - font_glyphes: (Glyph.key, Glyph.cell) Hashtbl.t; - font_todo: (Glyph.key, unit) Hashtbl.t; + font_glyphes: Glyph.cell Glyphtbl.t; + font_todo: unit Glyphtbl.t; mutable font_buffer: font_buffer option; } + let font_stash () = { - font_glyphes = Hashtbl.create 8; - font_todo = Hashtbl.create 8; + font_glyphes = Glyphtbl.create 8; + font_todo = Glyphtbl.create 8; font_buffer = None; } @@ -215,7 +238,7 @@ let render_glyphes stash _ xform (font,pos,text) quad ~(push : unit -> unit) = | -1 -> last := Stb_truetype.invalid_glyph | cp -> let key = key cp in - match Hashtbl.find stash.font_glyphes key with + match Glyphtbl.find stash.font_glyphes key with | cell when cell == null_cell -> last := Stb_truetype.invalid_glyph | { Glyph. box; uv; glyph; _ } -> @@ -274,7 +297,7 @@ let bake_glyphs renderer t = let add_box ({ Glyph. scale; cp; ttf; blur } as key) () boxes = match Stb_truetype.find ttf cp with | None -> - Hashtbl.add t.font_glyphes key null_cell; + Glyphtbl.add t.font_glyphes key null_cell; boxes | Some glyph -> let scale = Stb_truetype.scale_for_pixel_height ttf (float scale /. 10.0) in @@ -290,16 +313,16 @@ let bake_glyphs renderer t = in box :: boxes in - let todo = Hashtbl.fold add_box t.font_todo [] in + let todo = Glyphtbl.fold add_box t.font_todo [] in let room, boxes = Maxrects.insert_batch buffer.room todo in let room, boxes = if List.exists (function None -> true | _ -> false) boxes then ( - let todo = Hashtbl.fold + let todo = Glyphtbl.fold (fun key cell todo -> if cell.Glyph.frame = !frame_nr then add_box key () todo else todo) t.font_glyphes todo in - Hashtbl.reset t.font_glyphes; + Glyphtbl.reset t.font_glyphes; Bigarray.Array1.fill (Stb_image.data buffer.image) 0; let room = Maxrects.add_bin () (Stb_image.width buffer.image) @@ -339,13 +362,13 @@ let bake_glyphs renderer t = uv, box ) in - Hashtbl.add t.font_glyphes key { Glyph. box; uv; frame = !frame_nr; glyph } + Glyphtbl.add t.font_glyphes key { Glyph. box; uv; frame = !frame_nr; glyph } ) boxes; - Hashtbl.reset t.font_todo; + Glyphtbl.reset t.font_todo; Texture.update buffer.texture buffer.image; incr frame_nr -let has_todo stash = Hashtbl.length stash.font_todo > 0 +let has_todo stash = Glyphtbl.length stash.font_todo > 0 let allocate_glyphes stash renderer ~sx ~sy (font,_pos,text) = let _, key = Glyph.key sx sy font in @@ -358,12 +381,12 @@ let allocate_glyphes stash renderer ~sx ~sy (font,_pos,text) = | -1 -> () | cp -> let key = key cp in - match Hashtbl.find stash.font_glyphes key with + match Glyphtbl.find stash.font_glyphes key with | cache -> cache.Glyph.frame <- frame_nr | exception Not_found -> - if not (Hashtbl.mem stash.font_todo key) then + if not (Glyphtbl.mem stash.font_todo key) then (*(prerr_endline ("new glyph: " ^ string_of_int cp);*) - (Hashtbl.add stash.font_todo key ()) + (Glyphtbl.add stash.font_todo key ()) done; if not has_todo0 && (has_todo stash) then Some (fun () -> bake_glyphs renderer stash) diff --git a/lib/wall_text.mli b/lib/wall_text.mli index d4b33a4..bb272cc 100644 --- a/lib/wall_text.mli +++ b/lib/wall_text.mli @@ -12,6 +12,7 @@ module Font : sig type t = { glyphes : Stb_truetype.t; + glyphes_id : int; size : float; blur : float; spacing : float; From 611de6756c7e7ba8050194e275ea62b37d3c1f91 Mon Sep 17 00:00:00 2001 From: Ty Overby Date: Sat, 5 Aug 2023 17:14:48 -0400 Subject: [PATCH 2/7] _ --- benchmarks/.ocamlformat | 2 + benchmarks/bench.ml | 134 ++++++++++++++++++++++++++++++++++ benchmarks/dune | 4 + benchmarks/lots_of_text.ml | 78 ++++++++++++++++++++ benchmarks/lots_of_text.mli | 1 + benchmarks/main.ml | 24 ++++++ benchmarks/many_graphs.ml | 141 ++++++++++++++++++++++++++++++++++++ benchmarks/many_graphs.mli | 1 + lib/wall.ml | 2 - lib/wall__backend.ml | 2 - lib/wall__geom.ml | 1 - lib/wall_text.ml | 1 - 12 files changed, 385 insertions(+), 6 deletions(-) create mode 100644 benchmarks/.ocamlformat create mode 100644 benchmarks/bench.ml create mode 100644 benchmarks/dune create mode 100644 benchmarks/lots_of_text.ml create mode 100644 benchmarks/lots_of_text.mli create mode 100644 benchmarks/main.ml create mode 100644 benchmarks/many_graphs.ml create mode 100644 benchmarks/many_graphs.mli diff --git a/benchmarks/.ocamlformat b/benchmarks/.ocamlformat new file mode 100644 index 0000000..5c1f1b1 --- /dev/null +++ b/benchmarks/.ocamlformat @@ -0,0 +1,2 @@ +profile=janestreet +version = 0.26.0 diff --git a/benchmarks/bench.ml b/benchmarks/bench.ml new file mode 100644 index 0000000..6aa345b --- /dev/null +++ b/benchmarks/bench.ml @@ -0,0 +1,134 @@ +open! Tsdl +open! Tgles2 +open! Wall +module I = Image +module P = Path +module Text = Wall_text + +module type S = sig + type state + + val name : string + val init : Wall.Renderer.t -> state + val frame : state -> width:float -> height:float -> elapsed_seconds:float -> I.t +end + +let load_font name = + let ic = open_in_bin name in + let dim = in_channel_length ic in + let fd = Unix.descr_of_in_channel ic in + let buffer = + Unix.map_file fd Bigarray.int8_unsigned Bigarray.c_layout false [| dim |] + |> Bigarray.array1_of_genarray + in + let offset = List.hd (Stb_truetype.enum buffer) in + match Stb_truetype.init buffer offset with + | None -> assert false + | Some font -> font +;; + +let font_sans = lazy (load_font "../example/Roboto-Regular.ttf") + +let run (module T : S) = + let window_width = 1000 in + let window_height = 800 in + Printexc.record_backtrace true; + match Sdl.init Sdl.Init.video with + | Error (`Msg e) -> + Sdl.log "Init error: %s" e; + exit 1 + | Ok () -> + ignore (Sdl.gl_set_attribute Sdl.Gl.depth_size 24 : _ result); + ignore (Sdl.gl_set_attribute Sdl.Gl.stencil_size 8 : _ result); + (match + Sdl.create_window + ~w:window_width + ~h:window_height + "SDL OpenGL" + Sdl.Window.(opengl + allow_highdpi) + with + | Error (`Msg e) -> + Sdl.log "Create window error: %s" e; + exit 1 + | Ok w -> + ignore (Sdl.gl_set_swap_interval 0); + let ow, oh = Sdl.gl_get_drawable_size w in + Sdl.log + "window size: %d,%d\topengl drawable size: %d,%d" + window_width + window_height + ow + oh; + let _sw = float ow /. float window_width + and _sh = float oh /. float window_height in + ignore (Sdl.gl_set_attribute Sdl.Gl.stencil_size 1); + (match Sdl.gl_create_context w with + | Error (`Msg e) -> + Sdl.log "Create context error: %s" e; + exit 1 + | Ok ctx -> + let context = Renderer.create ~antialias:true () in + let state = T.init context in + let quit = ref false in + let event = Sdl.Event.create () in + let prev_frame_fps = ref 0.0 in + let freq = Sdl.get_performance_frequency () in + let font_sans = Lazy.force font_sans in + while not !quit do + let timing_start = Sdl.get_performance_counter () in + while Sdl.poll_event (Some event) do + match Sdl.Event.enum (Sdl.Event.get event Sdl.Event.typ) with + | `Quit -> quit := true + | _ -> () + done; + Gl.viewport 0 0 ow oh; + Gl.clear_color 0.3 0.3 0.32 1.0; + Gl.(clear (color_buffer_bit lor depth_buffer_bit lor stencil_buffer_bit)); + Gl.enable Gl.blend; + Gl.blend_func_separate Gl.one Gl.src_alpha Gl.one Gl.one_minus_src_alpha; + Gl.enable Gl.cull_face_enum; + Gl.disable Gl.depth_test; + let elapsed_seconds = Int32.to_float (Sdl.get_ticks ()) /. 1000.0 in + let () = + let width = float window_width in + let height = float window_height in + let image = T.frame state ~width ~height ~elapsed_seconds in + let fps = + let fps = Printf.sprintf " FPS: %d" (Float.to_int !prev_frame_fps) in + I.stack + (I.paint + (Paint.color (Color.v 0.0 0.0 0.0 1.0)) + Text.( + simple_text + (Font.make ~blur:2.0 ~size:30.0 font_sans) + ~valign:`TOP + ~halign:`LEFT + ~x:0.0 + ~y:0.0 + fps)) + (I.paint + (Paint.color (Color.v 1.0 1.0 1.0 1.0)) + Text.( + simple_text + (Font.make ~size:30.0 font_sans) + ~valign:`TOP + ~halign:`LEFT + ~x:0.0 + ~y:0.0 + fps)) + in + Renderer.render context ~width ~height (I.stack image fps) + in + Sdl.gl_swap_window w; + let timing_end = Sdl.get_performance_counter () in + let seconds_elapsed = + Int64.to_float (Int64.sub timing_end timing_start) /. Int64.to_float freq + in + prev_frame_fps := 1.0 /. seconds_elapsed; + () + done; + Sdl.gl_delete_context ctx; + Sdl.destroy_window w; + Sdl.quit (); + exit 0)) +;; diff --git a/benchmarks/dune b/benchmarks/dune new file mode 100644 index 0000000..abe9b96 --- /dev/null +++ b/benchmarks/dune @@ -0,0 +1,4 @@ +(executables + (names main) + (flags :standard -w -3-6-27) + (libraries tsdl tgls.tgles2 wall)) diff --git a/benchmarks/lots_of_text.ml b/benchmarks/lots_of_text.ml new file mode 100644 index 0000000..36e45ab --- /dev/null +++ b/benchmarks/lots_of_text.ml @@ -0,0 +1,78 @@ +open Wall +module I = Image +module Text = Wall_text + +type command = + { color : Color.t + ; text : string + ; leave : bool + ; x : float + ; y : float + } + +type state = + { font : Text.Font.t + ; commands : command list + } + +let init _ctx = + let font = + let tt = Lazy.force Bench.font_sans in + Text.Font.make ~size:30.0 ~placement:`Subpixel tt + in + let commands = ref [] in + let push i = commands := i :: !commands in + for x = 0 to 100 do + let x = Float.of_int x *. 100.0 in + for y = 0 to 100 do + let y = Float.of_int y *. 50.0 in + let c1, c2 = + ( Color.v + (Random.float 1.0) + (Random.float 1.0) + (Random.float 1.0) + (Random.float 1.0) + , Color.v + (Random.float 1.0) + (Random.float 1.0) + (Random.float 1.0) + (Random.float 1.0) ) + in + push { color = c1; text = "hello"; leave = Random.bool (); x; y }; + push { color = c2; text = "world"; leave = Random.bool (); x; y } + done + done; + { font; commands = !commands } +;; + +let paint_text ~x ~y ~color ~font s = + I.paint (Paint.color color) (Text.simple_text font ~valign:`TOP ~halign:`LEFT ~x ~y s) +;; + +let frame { font; commands } ~width ~height ~elapsed_seconds = + let matrix = + let scale = + Transform.scale + ~sx:((Float.sin elapsed_seconds +. 1.25) /. 2.0) + ~sy:((Float.sin elapsed_seconds +. 1.25) /. 2.0) + in + let forward = + Transform.translation + ~x:((-.width /. 2.0) -. 5000.0) + ~y:((-.height /. 2.0) -. 2500.0) + in + let backward = Transform.translation ~x:(width /. 2.0) ~y:(height /. 2.0) in + Transform.compose forward (Transform.compose scale backward) + in + let should_disappear = Float.to_int (elapsed_seconds /. 2.0) mod 2 = 0 in + List.filter_map + (fun { color; text; leave; x; y } -> + if should_disappear && leave + then None + else Some (paint_text ~x ~y ~color ~font text)) + commands + |> I.seq + |> I.transform matrix +;; + +let name = "lots-of-text" diff --git a/benchmarks/lots_of_text.mli b/benchmarks/lots_of_text.mli new file mode 100644 index 0000000..2c850be --- /dev/null +++ b/benchmarks/lots_of_text.mli @@ -0,0 +1 @@ +include Bench.S diff --git a/benchmarks/main.ml b/benchmarks/main.ml new file mode 100644 index 0000000..d385636 --- /dev/null +++ b/benchmarks/main.ml @@ -0,0 +1,24 @@ +let benches : (module Bench.S) list = [ (module Lots_of_text); (module Many_graphs) ] + +let print_usage argv0 = + print_endline "Usage:"; + benches + |> List.iter (fun (module M : Bench.S) -> + print_endline (Printf.sprintf " %s %s" argv0 M.name)) +;; + +let () = + match Array.get Sys.argv 1 with + | arg -> + let bench_to_run = + benches |> List.find_opt (fun (module M : Bench.S) -> String.equal M.name arg) + in + (match bench_to_run with + | None -> + print_usage (Array.get Sys.argv 0); + exit (-1) + | Some bench -> Bench.run bench) + | exception _ -> + print_usage (Array.get Sys.argv 0); + exit (-1) +;; diff --git a/benchmarks/many_graphs.ml b/benchmarks/many_graphs.ml new file mode 100644 index 0000000..36a5ae4 --- /dev/null +++ b/benchmarks/many_graphs.ml @@ -0,0 +1,141 @@ +open Wall +module I = Image +module P = Path + +type state = unit + +let init _ctx = () + +let draw_graph x y w h t = + let samples = + [| (1.0 +. sin ((t *. 1.2345) +. (cos (t *. 0.33457) *. 0.44))) *. 0.5 + ; (1.0 +. sin ((t *. 0.68363) +. (cos (t *. 1.3) *. 1.55))) *. 0.5 + ; (1.0 +. sin ((t *. 1.1642) +. (cos (t *. 0.33457) *. 1.24))) *. 0.5 + ; (1.0 +. sin ((t *. 0.56345) +. (cos (t *. 1.63) *. 0.14))) *. 0.5 + ; (1.0 +. sin ((t *. 1.6245) +. (cos (t *. 0.254) *. 0.3))) *. 0.5 + ; (1.0 +. sin ((t *. 0.345) +. (cos (t *. 0.03) *. 0.6))) *. 0.5 + |] + in + let dx = w /. 5.0 in + let sx i = x +. (float i *. dx) in + let sy i = y +. (h *. samples.(i) *. 0.8) in + I.seq + [ (* Graph background *) + I.paint + (Paint.linear_gradient + ~sx:x + ~sy:y + ~ex:x + ~ey:(y +. h) + ~inner:(Color.v 0.00 0.60 0.75 0.00) + ~outer:(Color.v 0.00 0.60 0.75 0.25)) + (I.fill_path + @@ fun t -> + P.move_to t ~x:(sx 0) ~y:(sy 0); + for i = 1 to 5 do + P.bezier_to + t + ~c1x:(sx (i - 1) +. (dx *. 0.5)) + ~c1y:(sy (i - 1)) + ~c2x:(sx i -. (dx *. 0.5)) + ~c2y:(sy i) + ~x:(sx i) + ~y:(sy i) + done; + P.line_to t ~x:(x +. w) ~y:(y +. h); + P.line_to t ~x ~y:(y +. h)) + ; (* Graph line *) + I.paint + (Paint.color (Color.v 0.0 0.0 0.0 0.125)) + (I.stroke_path Outline.{ default with stroke_width = 3.0 } + @@ fun t -> + P.move_to t ~x:(sx 0) ~y:(sy 0 +. 2.0); + for i = 1 to 5 do + P.bezier_to + t + ~c1x:(sx (i - 1) +. (dx *. 0.5)) + ~c1y:(sy (i - 1) +. 2.0) + ~c2x:(sx i -. (dx *. 0.5)) + ~c2y:(sy i +. 2.0) + ~x:(sx i) + ~y:(sy i +. 2.0) + done) + ; I.paint + (Paint.color (Color.v 0.0 0.60 0.75 1.0)) + (I.stroke_path Outline.{ default with stroke_width = 3.0 } + @@ fun t -> + P.move_to t ~x:(sx 0) ~y:(sy 0); + for i = 1 to 5 do + P.bezier_to + t + ~c1x:(sx (i - 1) +. (dx *. 0.5)) + ~c1y:(sy (i - 1)) + ~c2x:(sx i -. (dx *. 0.5)) + ~c2y:(sy i) + ~x:(sx i) + ~y:(sy i) + done) + ; (* Graph sample pos *) + (let node = ref I.empty in + for i = 0 to 5 do + node + := I.stack + !node + (I.paint + (Paint.radial_gradient + ~cx:(sx i) + ~cy:(sy i +. 2.0) + ~inr:3.0 + ~outr:8.0 + ~inner:(Color.v 0.0 0.0 0.0 0.125) + ~outer:(Color.v 0.0 0.0 0.0 0.0)) + (I.fill_path + @@ fun t -> + P.rect t ~x:(sx i -. 10.0) ~y:(sy i -. 10.0 +. 2.0) ~w:20.0 ~h:20.0)) + done; + !node) + ; I.paint + (Paint.color (Color.v 0.0 0.6 0.75 1.0)) + (I.fill_path + @@ fun t -> + for i = 0 to 5 do + P.circle t ~cx:(sx i) ~cy:(sy i) ~r:4.0 + done) + ; I.paint + (Paint.color (Color.v 0.8 0.8 0.8 1.0)) + (I.fill_path + @@ fun t -> + for i = 0 to 5 do + P.circle t ~cx:(sx i) ~cy:(sy i) ~r:2.0 + done) + ] +;; + +let many_graphs ~width:w ~height:h t = + let node = ref I.empty in + let push n = node := I.stack !node n in + for i = 0 to 500 do + push @@ draw_graph 0.0 0.0 w h (t +. float i) + done; + !node +;; + +let many_graphs_cached = ref None + +let many_graphs ~width ~height time = + match !many_graphs_cached with + | Some (w, h, t, cached) + when Float.equal w width && Float.equal h height && Float.equal t time -> cached + | _ -> + let cached = many_graphs ~width ~height time in + many_graphs_cached := Some (width, height, time, cached); + cached +;; + +let frame () ~width ~height ~elapsed_seconds = + I.stack + (many_graphs ~width ~height 0.0) + (draw_graph 0.0 0.0 width height elapsed_seconds) +;; + +let name = "many-graphs" diff --git a/benchmarks/many_graphs.mli b/benchmarks/many_graphs.mli new file mode 100644 index 0000000..2c850be --- /dev/null +++ b/benchmarks/many_graphs.mli @@ -0,0 +1 @@ +include Bench.S diff --git a/lib/wall.ml b/lib/wall.ml index ae5118b..1a4bca4 100644 --- a/lib/wall.ml +++ b/lib/wall.ml @@ -20,8 +20,6 @@ open Gg open Wall_types -let compare = () - module Backend = Wall__backend type renderer = { diff --git a/lib/wall__backend.ml b/lib/wall__backend.ml index da2d124..379eb2a 100644 --- a/lib/wall__backend.ml +++ b/lib/wall__backend.ml @@ -2,8 +2,6 @@ open Wall_types open Gg open Bigarray -let compare = () - type state external wall_gl_create diff --git a/lib/wall__geom.ml b/lib/wall__geom.ml index 068008d..00c58bb 100644 --- a/lib/wall__geom.ml +++ b/lib/wall__geom.ml @@ -15,7 +15,6 @@ misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. *) -let compare = () [@@@landmark "auto"] module BA = Bigarray.Array1 diff --git a/lib/wall_text.ml b/lib/wall_text.ml index e6a66b1..3953fd9 100644 --- a/lib/wall_text.ml +++ b/lib/wall_text.ml @@ -1,6 +1,5 @@ open Wall open Wall__geom -let compare = () (* utf-8 decoding dfa, from http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ *) From 4119a192ad25a9030a2d7f108f24b1f2510a2f6d Mon Sep 17 00:00:00 2001 From: Ty Overby Date: Sat, 5 Aug 2023 17:15:26 -0400 Subject: [PATCH 3/7] removed example benches --- example/bench.ml | 107 --------------------------- example/benchmarks.ml | 164 ------------------------------------------ 2 files changed, 271 deletions(-) delete mode 100644 example/bench.ml delete mode 100644 example/benchmarks.ml diff --git a/example/bench.ml b/example/bench.ml deleted file mode 100644 index 0c0dd30..0000000 --- a/example/bench.ml +++ /dev/null @@ -1,107 +0,0 @@ -open! Tsdl -open! Tgles2 - -open! Wall -module I = Image -module P = Path -module Text = Wall_text - -module type S = sig - type state - val init : Wall.Renderer.t -> state - val frame : state -> width:float -> height:float -> elapsed_seconds:float -> I.t -end - -let load_font name = - let ic = open_in_bin name in - let dim = in_channel_length ic in - let fd = Unix.descr_of_in_channel ic in - let buffer = - Unix.map_file fd Bigarray.int8_unsigned Bigarray.c_layout false [|dim|] - |> Bigarray.array1_of_genarray - in - let offset = List.hd (Stb_truetype.enum buffer) in - match Stb_truetype.init buffer offset with - | None -> assert false - | Some font -> font - -let font_sans = lazy (load_font "Roboto-Regular.ttf") - -let run (module T: S) = - let window_width = 1000 in - let window_height = 800 in - Printexc.record_backtrace true; - match Sdl.init Sdl.Init.video with - | Error (`Msg e) -> Sdl.log "Init error: %s" e; exit 1 - | Ok () -> - ignore (Sdl.gl_set_attribute Sdl.Gl.depth_size 24 : _ result); - ignore (Sdl.gl_set_attribute Sdl.Gl.stencil_size 8 : _ result); - match - Sdl.create_window ~w:window_width ~h:window_height "SDL OpenGL" - Sdl.Window.(opengl + allow_highdpi) - with - | Error (`Msg e) -> Sdl.log "Create window error: %s" e; exit 1 - | Ok w -> - (*Sdl.gl_set_attribute Sdl.Gl.context_profile_mask Sdl.Gl.context_profile_core; - Sdl.gl_set_attribute Sdl.Gl.context_major_version 2; - Sdl.gl_set_attribute Sdl.Gl.context_minor_version 1;*) - ignore (Sdl.gl_set_swap_interval (-1)); - let ow, oh = Sdl.gl_get_drawable_size w in - Sdl.log "window size: %d,%d\topengl drawable size: %d,%d" window_width window_height ow oh; - let _sw = float ow /. float window_width and _sh = float oh /. float window_height in - ignore (Sdl.gl_set_attribute Sdl.Gl.stencil_size 1); - match Sdl.gl_create_context w with - | Error (`Msg e) -> Sdl.log "Create context error: %s" e; exit 1 - | Ok ctx -> - let context = Renderer.create ~antialias:true () in - let state = T.init context in - let quit = ref false in - let event = Sdl.Event.create () in - let prev_frame_fps = ref 0.0 in - let freq = Sdl.get_performance_frequency () in - let font_sans = Lazy.force font_sans in - while not !quit do - let timing_start = Sdl.get_performance_counter () in - while Sdl.poll_event (Some event) do - match Sdl.Event.enum (Sdl.Event.get event Sdl.Event.typ) with - | `Quit -> quit := true - | _ -> () - done; - Gl.viewport 0 0 ow oh; - Gl.clear_color 0.3 0.3 0.32 1.0; - Gl.(clear (color_buffer_bit lor depth_buffer_bit lor stencil_buffer_bit)); - Gl.enable Gl.blend; - Gl.blend_func_separate Gl.one Gl.src_alpha Gl.one Gl.one_minus_src_alpha; - Gl.enable Gl.cull_face_enum; - Gl.disable Gl.depth_test; - let elapsed_seconds = (Int32.to_float (Sdl.get_ticks ()) /. 1000.0) in - let () = - let width = (float window_width) in - let height = (float window_height) in - let image = T.frame state ~width ~height ~elapsed_seconds in - let fps = - let fps = Printf.sprintf " FPS: %d" (Float.to_int !prev_frame_fps) in - I.stack - (I.paint (Paint.color (Color.v 0.0 0.0 0.0 1.0)) - Text.(simple_text - (Font.make ~blur:2.0 ~size:30.0 font_sans) - ~valign:`TOP ~halign:`LEFT - ~x:0.0 ~y:0.0 fps)) - (I.paint (Paint.color (Color.v 1.0 1.0 1.0 1.0)) - Text.(simple_text - (Font.make ~size:30.0 font_sans) - ~valign:`TOP ~halign:`LEFT - ~x:0.0 ~y:0.0 fps)) - in - Renderer.render context ~width ~height (I.stack image fps) - in - Sdl.gl_swap_window w; - let timing_end = Sdl.get_performance_counter () in - let seconds_elapsed = Int64.to_float (Int64.sub timing_end timing_start) /. (Int64.to_float freq) in - prev_frame_fps := (1.0 /. seconds_elapsed); - () - done; - Sdl.gl_delete_context ctx; - Sdl.destroy_window w; - Sdl.quit (); - exit 0 diff --git a/example/benchmarks.ml b/example/benchmarks.ml deleted file mode 100644 index b4217c1..0000000 --- a/example/benchmarks.ml +++ /dev/null @@ -1,164 +0,0 @@ -open Wall -module I = Image -module P = Path -module Text = Wall_text - - -module Many_graphs: Bench.S = struct - type state = unit - let init _ctx = () - - let draw_graph x y w h t = - let samples = [| - (1.0 +. sin (t *. 1.2345 +. cos (t *. 0.33457) *. 0.44 )) *. 0.5; - (1.0 +. sin (t *. 0.68363 +. cos (t *. 1.3 ) *. 1.55 )) *. 0.5; - (1.0 +. sin (t *. 1.1642 +. cos (t *. 0.33457) *. 1.24 )) *. 0.5; - (1.0 +. sin (t *. 0.56345 +. cos (t *. 1.63 ) *. 0.14 )) *. 0.5; - (1.0 +. sin (t *. 1.6245 +. cos (t *. 0.254 ) *. 0.3 )) *. 0.5; - (1.0 +. sin (t *. 0.345 +. cos (t *. 0.03 ) *. 0.6 )) *. 0.5; - |] in - let dx = w /. 5.0 in - let sx i = x +. float i *. dx in - let sy i = y +. h *. samples.(i) *. 0.8 in - I.seq [ - (* Graph background *) - I.paint - (Paint.linear_gradient ~sx:x ~sy:y ~ex:x ~ey:(y +. h) - ~inner:(Color.v 0.00 0.60 0.75 0.00) - ~outer:(Color.v 0.00 0.60 0.75 0.25)) - (I.fill_path @@ fun t -> - P.move_to t ~x:(sx 0) ~y:(sy 0); - for i = 1 to 5 do - P.bezier_to t - ~c1x:(sx (i - 1) +. dx *. 0.5) ~c1y:(sy (i - 1)) - ~c2x:(sx i -. dx *. 0.5) ~c2y:(sy i) - ~x:(sx i) ~y:(sy i) - done; - P.line_to t ~x:(x +. w) ~y:(y +. h); - P.line_to t ~x ~y:(y +. h)); - (* Graph line *) - I.paint (Paint.color (Color.v 0.0 0.0 0.0 0.125)) - (I.stroke_path Outline.{default with stroke_width = 3.0} @@ fun t -> - P.move_to t ~x:(sx 0) ~y:(sy 0 +. 2.0); - for i = 1 to 5 do - P.bezier_to t - ~c1x:(sx (i - 1) +. dx *. 0.5) ~c1y:(sy (i - 1) +. 2.0) - ~c2x:(sx i -. dx *. 0.5) ~c2y:(sy i +. 2.0) - ~x:(sx i) ~y:(sy i +. 2.0) - done); - I.paint (Paint.color (Color.v 0.0 0.60 0.75 1.0)) - (I.stroke_path Outline.{default with stroke_width = 3.0} @@ fun t -> - P.move_to t ~x:(sx 0) ~y:(sy 0); - for i = 1 to 5 do - P.bezier_to t - ~c1x:(sx (i - 1) +. dx *. 0.5) ~c1y:(sy (i - 1)) - ~c2x:(sx i -. dx *. 0.5) ~c2y:(sy i) - ~x:(sx i) ~y:(sy i) - done); - (* Graph sample pos *) - (let node = ref I.empty in - for i = 0 to 5 do - node := I.stack !node ( - I.paint - (Paint.radial_gradient ~cx:(sx i) ~cy:(sy i +. 2.0) ~inr:3.0 ~outr:8.0 - ~inner:(Color.v 0.0 0.0 0.0 0.125) ~outer:(Color.v 0.0 0.0 0.0 0.0)) - (I.fill_path @@ fun t -> - P.rect t ~x:(sx i -. 10.0) ~y:(sy i -. 10.0 +. 2.0) ~w:20.0 ~h:20.0)) - done; - !node); - I.paint (Paint.color (Color.v 0.0 0.6 0.75 1.0)) - (I.fill_path @@ fun t -> - for i = 0 to 5 do - P.circle t ~cx:(sx i) ~cy:(sy i) ~r:4.0; - done); - I.paint (Paint.color (Color.v 0.8 0.8 0.8 1.0)) - (I.fill_path @@ fun t -> - for i = 0 to 5 do - P.circle t ~cx:(sx i) ~cy:(sy i) ~r:2.0 - done) - ] - ;; - - let many_graphs ~width:w ~height:h t = - let node = ref I.empty in - let push n = node := I.stack !node n in - for i = 0 to 500 do - push @@ draw_graph 0.0 0.0 w h (t +. (float i)); - done; - !node - ;; - - let many_graphs_cached = ref None - - let many_graphs ~width ~height time = - match !many_graphs_cached with - | Some (w, h, t, cached) - when Float.equal w width - && Float.equal h height - && Float.equal t time -> cached - | _ -> - let cached = many_graphs ~width ~height time in - many_graphs_cached := Some (width, height, time, cached); - cached - - let frame () ~width ~height ~elapsed_seconds = - I.stack - (many_graphs ~width ~height 0.0) - (draw_graph 0.0 0.0 width height elapsed_seconds) -end - -module Lots_of_text: Bench.S = struct - type command = { - color: Color.t; - text : string; - leave: bool; - x: float; - y: float } - - type state = { - font : Text.Font.t; - commands : command list - } - - let init _ctx = - let font = - let tt = Lazy.force Bench.font_sans in - (Text.Font.make ~size:30.0 ~placement:`Subpixel tt) - in - let commands = ref [] in - let push i = commands := i :: !commands in - for x = 0 to 100 do - let x = Float.of_int x *. 100.0 in - for y = 0 to 100 do - let y = Float.of_int y *. 50.0 in - let c1, c2 = - Color.v (Random.float 1.0) (Random.float 1.0) (Random.float 1.0) (Random.float 1.0), - Color.v (Random.float 1.0) (Random.float 1.0) (Random.float 1.0) (Random.float 1.0) in - push {color = c1; text = "hello"; leave = Random.bool (); x; y}; - push {color = c2; text = "world"; leave = Random.bool (); x; y}; - done; - done; - {font; commands = !commands} - - let paint_text ~x ~y ~color ~font s = - I.paint - (Paint.color color) - (Text.simple_text font ~valign:`TOP ~halign:`LEFT ~x ~y s) - - let frame {font; commands} ~width ~height ~elapsed_seconds = - let matrix = - let scale = (Transform.scale ~sx:((Float.sin elapsed_seconds +. 1.25) /. 2.0) ~sy:((Float.sin elapsed_seconds +. 1.25) /. 2.0)) in - let forward = (Transform.translation ~x:(-. width /. 2.0 -. 5000.0) ~y:(-. height /. 2.0 -. 2500.0)) in - let backward = (Transform.translation ~x:(width /. 2.0) ~y:(height /. 2.0)) in - Transform.compose forward (Transform.compose scale backward) - (* Transform.compose sc tr *) - in - let should_disappear = (Float.to_int (elapsed_seconds /. 2.0)) mod 2 = 0 in - List.filter_map (fun {color; text; leave; x; y} -> - if should_disappear && leave then None - else Some (paint_text ~x ~y ~color ~font text)) commands - |> I.seq - |> I.transform matrix -end -(* let () = Bench.run (module Many_graphs) *) -let () = Bench.run (module Lots_of_text) From bddf432d713ad707de2460515df4b650609451d9 Mon Sep 17 00:00:00 2001 From: Ty Overby Date: Sat, 5 Aug 2023 17:26:39 -0400 Subject: [PATCH 4/7] remove old executable from examples dune file --- example/dune | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/dune b/example/dune index 39dc48e..eca315d 100644 --- a/example/dune +++ b/example/dune @@ -1,4 +1,4 @@ (executables - (names example minimal blemish colorweb benchmarks) + (names example minimal blemish colorweb) (flags :standard -w -3-6-27) (libraries tsdl tgls.tgles2 wall)) From bea1755992b1011920a837e6b159794b485dd6c2 Mon Sep 17 00:00:00 2001 From: Ty Overby Date: Sat, 5 Aug 2023 17:33:29 -0400 Subject: [PATCH 5/7] undo optimization attempts --- lib/wall.ml | 22 +++++++++---------- lib/wall_text.ml | 56 ++++++++++++++--------------------------------- lib/wall_text.mli | 1 - 3 files changed, 27 insertions(+), 52 deletions(-) diff --git a/lib/wall.ml b/lib/wall.ml index 1a4bca4..4fe2fbe 100644 --- a/lib/wall.ml +++ b/lib/wall.ml @@ -977,25 +977,23 @@ module Renderer = struct x0 = 0.0; y0 = 0.0; x1 = 0.0; y1 = 0.0; u0 = 0.0; v0 = 0.0; u1 = 0.0; v1 = 0.0} - let ( .%{}<-) (arr: B.bigarray) idx v = Bigarray.Array1.unsafe_set arr idx v;; - let push_quad b = let d = B.data b and c = B.alloc b (6 * 4) in let q = quadbuf in - d.%{c+ 0+0}<-q.x0; d.%{c+ 0+1}<-q.y0; d.%{c+ 0+2}<-q.u0; d.%{c+ 0+3}<-q.v0; - d.%{c+ 4+0}<-q.x1; d.%{c+ 4+1}<-q.y1; d.%{c+ 4+2}<-q.u1; d.%{c+ 4+3}<-q.v1; - d.%{c+ 8+0}<-q.x1; d.%{c+ 8+1}<-q.y0; d.%{c+ 8+2}<-q.u1; d.%{c+ 8+3}<-q.v0; - d.%{c+12+0}<-q.x0; d.%{c+12+1}<-q.y0; d.%{c+12+2}<-q.u0; d.%{c+12+3}<-q.v0; - d.%{c+16+0}<-q.x0; d.%{c+16+1}<-q.y1; d.%{c+16+2}<-q.u0; d.%{c+16+3}<-q.v1; - d.%{c+20+0}<-q.x1; d.%{c+20+1}<-q.y1; d.%{c+20+2}<-q.u1; d.%{c+20+3}<-q.v1 + d.{c+ 0+0}<-q.x0; d.{c+ 0+1}<-q.y0; d.{c+ 0+2}<-q.u0; d.{c+ 0+3}<-q.v0; + d.{c+ 4+0}<-q.x1; d.{c+ 4+1}<-q.y1; d.{c+ 4+2}<-q.u1; d.{c+ 4+3}<-q.v1; + d.{c+ 8+0}<-q.x1; d.{c+ 8+1}<-q.y0; d.{c+ 8+2}<-q.u1; d.{c+ 8+3}<-q.v0; + d.{c+12+0}<-q.x0; d.{c+12+1}<-q.y0; d.{c+12+2}<-q.u0; d.{c+12+3}<-q.v0; + d.{c+16+0}<-q.x0; d.{c+16+1}<-q.y1; d.{c+16+2}<-q.u0; d.{c+16+3}<-q.v1; + d.{c+20+0}<-q.x1; d.{c+20+1}<-q.y1; d.{c+20+2}<-q.u1; d.{c+20+3}<-q.v1 let push_quad_strip b = let d = B.data b and c = B.alloc b (4 * 4) in let q = quadbuf in - d.%{c+ 0+0}<-q.x1; d.%{c+ 0+1}<-q.y1; d.%{c+ 0+2}<-q.u1; d.%{c+ 0+3}<-q.v1; - d.%{c+ 4+0}<-q.x1; d.%{c+ 4+1}<-q.y0; d.%{c+ 4+2}<-q.u1; d.%{c+ 4+3}<-q.v0; - d.%{c+ 8+0}<-q.x0; d.%{c+ 8+1}<-q.y1; d.%{c+ 8+2}<-q.u0; d.%{c+ 8+3}<-q.v1; - d.%{c+12+0}<-q.x0; d.%{c+12+1}<-q.y0; d.%{c+12+2}<-q.u0; d.%{c+12+3}<-q.v0 + d.{c+ 0+0}<-q.x1; d.{c+ 0+1}<-q.y1; d.{c+ 0+2}<-q.u1; d.{c+ 0+3}<-q.v1; + d.{c+ 4+0}<-q.x1; d.{c+ 4+1}<-q.y0; d.{c+ 4+2}<-q.u1; d.{c+ 4+3}<-q.v0; + d.{c+ 8+0}<-q.x0; d.{c+ 8+1}<-q.y1; d.{c+ 8+2}<-q.u0; d.{c+ 8+3}<-q.v1; + d.{c+12+0}<-q.x0; d.{c+12+1}<-q.y0; d.{c+12+2}<-q.u0; d.{c+12+3}<-q.v0 let scale_factor xf = let sx = Utils.norm xf.x00 xf.x10 in diff --git a/lib/wall_text.ml b/lib/wall_text.ml index 3953fd9..f9a23fa 100644 --- a/lib/wall_text.ml +++ b/lib/wall_text.ml @@ -45,7 +45,6 @@ module Font = struct type t = { glyphes: Stb_truetype.t; - glyphes_id : int; size: float; blur: float; spacing: float; @@ -54,8 +53,7 @@ module Font = struct } let make ?(size=16.0) ?(blur=0.0) ?(spacing=0.0) ?(line_height=1.0) ?(placement=`Aligned) glyphes = - let glyphes_id = Oo.id (Obj.magic glyphes) in - { glyphes; glyphes_id; blur; size; spacing; line_height; placement } + { glyphes; blur; size; spacing; line_height; placement } type metrics = { ascent : float; @@ -150,16 +148,14 @@ module Glyph = struct cp : int; scale : int; ttf : Stb_truetype.t; - ttf_id : int; blur : int; } let key ~sx ~sy font = let ttf = font.Font.glyphes in - let ttf_id = font.Font.glyphes_id in let blur = decimal_quantize font.Font.blur in let factor, scale = estimate_scale sx sy font in - (factor, (fun cp -> { cp; scale; ttf; ttf_id; blur })) + (factor, (fun cp -> { cp; scale; ttf; blur })) type cell = { box : Stb_truetype.box; @@ -180,33 +176,15 @@ let null_cell = {Glyph. box = null_box; uv = null_box; glyph = Stb_truetype.invalid_glyph; frame = -1 } -module Glyphtbl = Hashtbl.Make (struct - type t = Glyph.key - - let equal (a :t) (b:t) = - a.cp == b.cp && - a.blur == b.blur && - a.scale == b.scale && - a.ttf_id == b.ttf_id - - let hash (a :t) = - let hash = a.cp in - let hash = Int.logxor (hash * 0x1f1f1f1f) a.blur in - let hash = Int.logxor (hash * 0x45d9f3b0) a.scale in - let hash = Int.logxor (hash * 0x119de1f3) a.ttf_id in - hash - -end) type font_stash = { - font_glyphes: Glyph.cell Glyphtbl.t; - font_todo: unit Glyphtbl.t; + font_glyphes: (Glyph.key, Glyph.cell) Hashtbl.t; + font_todo: (Glyph.key, unit) Hashtbl.t; mutable font_buffer: font_buffer option; } - let font_stash () = { - font_glyphes = Glyphtbl.create 8; - font_todo = Glyphtbl.create 8; + font_glyphes = Hashtbl.create 8; + font_todo = Hashtbl.create 8; font_buffer = None; } @@ -237,7 +215,7 @@ let render_glyphes stash _ xform (font,pos,text) quad ~(push : unit -> unit) = | -1 -> last := Stb_truetype.invalid_glyph | cp -> let key = key cp in - match Glyphtbl.find stash.font_glyphes key with + match Hashtbl.find stash.font_glyphes key with | cell when cell == null_cell -> last := Stb_truetype.invalid_glyph | { Glyph. box; uv; glyph; _ } -> @@ -296,7 +274,7 @@ let bake_glyphs renderer t = let add_box ({ Glyph. scale; cp; ttf; blur } as key) () boxes = match Stb_truetype.find ttf cp with | None -> - Glyphtbl.add t.font_glyphes key null_cell; + Hashtbl.add t.font_glyphes key null_cell; boxes | Some glyph -> let scale = Stb_truetype.scale_for_pixel_height ttf (float scale /. 10.0) in @@ -312,16 +290,16 @@ let bake_glyphs renderer t = in box :: boxes in - let todo = Glyphtbl.fold add_box t.font_todo [] in + let todo = Hashtbl.fold add_box t.font_todo [] in let room, boxes = Maxrects.insert_batch buffer.room todo in let room, boxes = if List.exists (function None -> true | _ -> false) boxes then ( - let todo = Glyphtbl.fold + let todo = Hashtbl.fold (fun key cell todo -> if cell.Glyph.frame = !frame_nr then add_box key () todo else todo) t.font_glyphes todo in - Glyphtbl.reset t.font_glyphes; + Hashtbl.reset t.font_glyphes; Bigarray.Array1.fill (Stb_image.data buffer.image) 0; let room = Maxrects.add_bin () (Stb_image.width buffer.image) @@ -361,13 +339,13 @@ let bake_glyphs renderer t = uv, box ) in - Glyphtbl.add t.font_glyphes key { Glyph. box; uv; frame = !frame_nr; glyph } + Hashtbl.add t.font_glyphes key { Glyph. box; uv; frame = !frame_nr; glyph } ) boxes; - Glyphtbl.reset t.font_todo; + Hashtbl.reset t.font_todo; Texture.update buffer.texture buffer.image; incr frame_nr -let has_todo stash = Glyphtbl.length stash.font_todo > 0 +let has_todo stash = Hashtbl.length stash.font_todo > 0 let allocate_glyphes stash renderer ~sx ~sy (font,_pos,text) = let _, key = Glyph.key sx sy font in @@ -380,12 +358,12 @@ let allocate_glyphes stash renderer ~sx ~sy (font,_pos,text) = | -1 -> () | cp -> let key = key cp in - match Glyphtbl.find stash.font_glyphes key with + match Hashtbl.find stash.font_glyphes key with | cache -> cache.Glyph.frame <- frame_nr | exception Not_found -> - if not (Glyphtbl.mem stash.font_todo key) then + if not (Hashtbl.mem stash.font_todo key) then (*(prerr_endline ("new glyph: " ^ string_of_int cp);*) - (Glyphtbl.add stash.font_todo key ()) + (Hashtbl.add stash.font_todo key ()) done; if not has_todo0 && (has_todo stash) then Some (fun () -> bake_glyphs renderer stash) diff --git a/lib/wall_text.mli b/lib/wall_text.mli index bb272cc..d4b33a4 100644 --- a/lib/wall_text.mli +++ b/lib/wall_text.mli @@ -12,7 +12,6 @@ module Font : sig type t = { glyphes : Stb_truetype.t; - glyphes_id : int; size : float; blur : float; spacing : float; From 3e67d25b4991928cb23c50639fa00f36ff3c16b3 Mon Sep 17 00:00:00 2001 From: Ty Overby Date: Sat, 5 Aug 2023 17:42:18 -0400 Subject: [PATCH 6/7] remove many-graphs from example --- example/example.ml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/example/example.ml b/example/example.ml index 88b12c7..a3ee238 100644 --- a/example/example.ml +++ b/example/example.ml @@ -782,23 +782,9 @@ let draw_thumbnails ~x ~y ~w ~h images t = ~w:(8.-.2.) ~h:(scrollh-.2.) ~r:2.) ] -let w = 1280 - -let h = 960 - -let many_graphs = - let t = 0.0 in - let node = ref I.empty in - let push n = node := I.stack !node n in - for i = 0 to 500 do - push @@ draw_graph 0.0 (float h /. 2.0) (float w) (float h /. 2.0) (t +. (float i)); - done; - !node - let draw_demo mx my w h t images = ( let node = ref I.empty in let push n = node := I.stack !node n in - push @@ many_graphs; push @@ draw_eyes (w -. 250.0) 50.0 150.0 100.0 mx my t; push @@ draw_graph 0.0 (h /. 2.0) w (h /. 2.0) t; push @@ draw_colorwheel (w -. 300.0) (h -. 300.0) 250.0 250.0 t; From 7f9091f264d7ae155957230100029437a129c3aa Mon Sep 17 00:00:00 2001 From: Ty Overby Date: Sun, 6 Aug 2023 12:34:11 -0400 Subject: [PATCH 7/7] add source-code benchmark --- benchmarks/RobotoMono-Light.ttf | Bin 0 -> 87592 bytes benchmarks/bench.ml | 16 +++++- benchmarks/lots_of_text.ml | 1 + benchmarks/main.ml | 4 +- benchmarks/many_graphs.ml | 1 + benchmarks/source_code.ml | 97 ++++++++++++++++++++++++++++++++ benchmarks/source_code.mli | 1 + 7 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 benchmarks/RobotoMono-Light.ttf create mode 100644 benchmarks/source_code.ml create mode 100644 benchmarks/source_code.mli diff --git a/benchmarks/RobotoMono-Light.ttf b/benchmarks/RobotoMono-Light.ttf new file mode 100644 index 0000000000000000000000000000000000000000..f03a2b9e4ca57d36aa256f2ef48b47e5f72c0e47 GIT binary patch literal 87592 zcmb?^2Y4LSweZ}T*T0!J+TMHBYPH%`?_IKbwJgb&Ww}f4#u$SQ*p$$XF~oo| zruT%<5(r61LVF3kgybb8Aq7%MNWj*P{&Qzm>lGzm-v9r$B(KignYs7e)6cob1R@9` zlQ=+7#7Ip?S9$e^`d<>Lql6&HGc6rX&rKaKZX=NXLl`yE(OK5{o5t771nT+?{9d_c zW^~j2C*F95K!!$w5R6QYZrud%Y`EVC|E`$auyZ2qg%2$7`9lKb{bp+I=s5D9$Rv>R z1%L&n0FeF&vcU6Sh2M2kGu!430{<4c{wX}~^$oLYM*paHtS6A@ulV?kHu`?w z-O*?rvKkE2VHD_46>3p)S=qh4%+@(H9NT8M`TP$1d;;&?AkeTghp{4dC2+;-!ONu^=T`Zxw!Iy- z8_kwbt}q*+Y+-J(**sg*vBzffubNv`R}wk1uIH*iuvI3LAp}2KiUL=6Pu&|SX+Ym- zIOKK*U1rmCB)l*Dc6e_|$+XGjL?O55P{a8B(b8(XB^cWJg_5xY)|e(j@Zc=&XAY(k^fA)QvYwW0Ke zF6Pep4`Q1fRpW(HIYLs2tUcg6)bau8v0Hc5)|~D+)^|^B^M0$|iYAd#rr%jta=7~1 z>O;k48!(Omr{9Cw=wUW10jGX-jymy8eLKFF{GWyUsDZeg{Kh&`yZ(IZda{*(JKlmj zoN!0l#XGz?)Q{qO$lorUo+Hn!Cw(xOJcGyXfE}yneTS$Zh_#ejq^8)_;5BkzejnZp z`0}Rin)ZvkCgygno@D%}7b#mD_ECGyR+~L066GObp0LDfovm(~v)MwKJ61MDO79-+ zJ{AnM%jJL-Wr1RKap>BfhT}1J--D!JJ#)n1&dOYgkQ47;$$`iZ!+XQw^+uD!Wwp*% zkDLsJO8s`no|>9ln|{!8qPA`?>>g68^2_m>PQm&e9L7d-v^i?{_QFrvu{qwcX%qPX z@8S4X@@l}qH{#WJ{EIMt6*s;^1Q-@^IDQe0F(;`8=6W>4dw9bIm_hK6P}F`L%|Cu z_*aWi*{$1GAWJigVa`8*`|pDL?QlO(7sf8a&p@<& zK0ZD-N8ZkR_(F*O?)g^w`xgQr)`>lUt5%>dJ<))vafzQdx5#a8-BrRe?aWR3^nDK-z*Kt67Ltx*cWZ<`$XF8>?$(uoTI~QUo9t^GtR1 zMza}1$oUuQZ>y_4;Bp3oPWS%0+S}@1Xt=euZokVF47yzVYwK=B87~#@Es1O}TU;)< zwWMTk@k^mSk&+D-liOuB&x9j;@LJym%*+C2vWY@iD<`%62|*1=S}vPWE9w}&H(FX( z8!bIE%mn68N9|sR9po{PHQ9rTgMr(}*Pbj2hDq3l=gFhUX|?WbsNZ2W+d++z#PlLb zPXT64MDu@uSrp_H$Cn`2|9Kt<L=42o+t^Yp&NIVf$xg zL#l$p9VkZ#tZmy;U$@0-iwN025vz4eUENlzHIkQ`OJ4Fo(;fA7mvHDkP+xaP(*sQ> z>*@~xdNWosO!J5KOPiYRL+GK%C6Uq@v)Kh0-54poq~xKRs;n`iYZ)U40xU905x11AN!Pn^|?6pw3jLPfUcP za#nJFiP%gkI?#30FG}35J+Y?Cyx#J(opgPy=EaHCXDZ6u+hP@`T8sDl{S_$ea?M5R zcG~QL6}zaWXzbp>j?-mjZD@66>8*8+1eBSGeslvCyN_Pvi9s0dBVeZUvZ{uqN>4 z(AzHj5xs%7^QzacKmQG0HHfshqv^?TtMg&D?r9z zvq@k?TGF}+y}6$`nlECm+z(==t+9cA7{+$N*jr(2E*>ikiiKXGmJ%3YI`{|D%)g$M z^`p5TZaSWu_v*Uu<>g$nnO@u3dEqvA#5=op)20jW?Al42F$O@mv-TR+wD3|ac(Lpy zw=>sWjhTeLC(azaV2>$zK?vL{2|=k2zO6#bH;bWG~m`AX8)Dw zEpih=ZyzCVrX&m7;y)*K)KxJ45*V+8@gPUnA|oIii9uFpka2S1TJrrk4^elmr$6dl zcR@S^a+@U10_M$hGvUV+IO@fsG>0X$ge(=Os%a4i6y@auktHN+e!iYH#{33WrF%Is zOWk)MQ;1NksOZMkt8WU0DyezWV>YcXj?S1&4)mwBPghlU6uDf78cJq8?v6roC3A^? zK%+IHfTQSC?|+NyU9K$|?%4pjvgeT9YI9kv+p3VaO`#}I7ZwgicInOLO9!jA7)^Gm zL|kFEw0jHW@)lR5Q>jy+k6bM<5LWjprYk-Hd<~XJS(1+uYs1Pfm_l7(j~G9%C2~^N z4;SZVRNPG^xy@*@Uv;5>0m?H4MPMzv{kW%$w_k#EWw7*H$Ka+$2o zj{fd!Q7Tj_dEsEd&GOToup%|=mlyVzGlDq__)8>0!uCz<6K6P+J4Y@vTMY;qtd>K= zv001RO>d#fi$b^b%{^QkE=H(09DaDN{ifnjExm>3vsh=^8%s-1v|HLvL?ewBy0id!1$7x9c1G0#4__rs5%$Mhh6noXi7^i?Nm4agkEQUHxIcPnHTG7JE;p{xBFn(%I3@5t|0K|6TqKi zd=;U>+(v;(co7xBRK62o*8SOD5NT+bJD4__&+>K4vV8z@=G?l3d=;e7zduJSNSQ;*F2Nw! zD8bTR-4Q7ApPUY?wdtc0tI0Cg>@JJ=O0d1YFEMG1!FA3j0 zP8)C17K;?hk>;kC_KR9iL}ML=3Pq!nq3yL`sMVK5Ph%PT5@1yhREI4j%+(354PZGX zhF?=wg^K6rbD|deY-!Cpty+cVk;~yc(px$2Hdj!SW#BWU8U)KXD)~MjVq`~` zyG&p}W_e@^e{P!GDUk&VMsD`|f_GTlF00Myvf{V82lp9G8bl%>*r?`3fV z_k!-Zh?)1$CCq-n!?k>3`utC(r+GKP$Ub<>4xr9r7zx`YWQA;y-7XQRIG1--g+OpDB z5_!F=cdLpnzVHeaDkzc23K1%h$|^JquTxIm3SL36Sgb@yEmu@)7k)?DW2&xGk<$A5(#WYU)oS)*r1TWb?qz0#w!!R_L=a}@%>gc3G6)j!j3gFN z>ZFXR>h$F~NmOT_ESfpx^ZHRR6u51DaKff9lkjHAzaYKYw71tYWHf}q6)@`iz1_Ra zHZwCqYE`~miFBZ`@zwRBEw4A#w8^BR68#PFk3=1(VGG%p9`5Bn) z9WYy7a<&(B5>Oq;hW}LN0j5vQct=3%%R%c)N_Doi!HuVVLGQfQX4>25>4&N2iAqg| zK3D%9+_K~ejuiqa;fTvlg`|G`=%7E4+p zF>rwqngaUr(k&1rBFYSWfzb%;K_zVvP!ZJC*S{M@f-QT|QVT5XM5 z<`fFEO!lZ6lfZ!^GM&WNBwkhobT^as%pS%S`dqf05gvsgr_g_Qha0RyL*}2#bV~xeRT1{_2m`R1{ezwQThao{R%I| zG4`yw@cFU7Z2X$m{AAZtCe=+F$zPv68!vzf7+-jSd^c`<`e_nGJ|Ygnlg8jlKs3v| zl+DZ|$I%I<;~F%A#*Q%^=)^VTLG*O|qxie%HO5C8Ng>?b$TZM<;cl!G*@#^baqbm@ z4M^+(D6sR@(`HjiKIy!IIb}35S6z-isL!?)6f`(;?cz*Ph-q-@^+Rs@gN0XzMk)V= zU!BxcE0sE$TFXm=x7+}8tcSONep!pKGl?LkhP>qa3pbG8kE^IBCaGP$Qwvx0O+qN0 zxC3^{2T@}w#Jj_=s#GIDl#}AZ=nbIzQr+rA^o=vG5l|HELv_u8+s3w^D+(3WwFPe- zojVsS3NHLbz%xVmq_28|(df+(BBR5wx2Iyg!Q{^*FXPdOj3L)m7uv$*18r?T-_5*! z^}jkJ(c!kXcXXy^4Ko#Fm0(J zh!{_#&mlj^VR-0kyocZ1dhYxm@E9ORS_sht$WM&=)u~~Ng#7g;MMpB6k3_UEPN5rp z6K0bSOn{Rk6JHM%$3Q{ZE#|qVs!e8-O{Y@z`Wfvc`hIA4S=kk}z1R7Dq3V*7Q+;*2 z9X7AkXxdmFn)U}amc)NE1oIeO{E+%P%mcjDG7%TVUS?%}Ni>6!P91iNWDJ!o?M^4!6~^t+Hyf z$z%&a*sGe7O(L~Ux4E@(tI=e%nN8bjMsEoOLZN{F*0I>E-CiyfRah-+g2Ok03N8r* zuJ5kgXfj%idc$l@<&;im1Rc@|m>z)LNM|FQ5Y_})eBw8_?pO3`i0~4@1Z?Lhv?BTVO=O9mE)uZ2STk@0?yl)al0t_53zd}NE9nX++ z9hYqqyKO-)#kosdf+*DwrG|6PEOxDLFOP;F*tq`AU?Ax8`A)7YTJLZ+EBKAIRj(h< z`Luc7Y{GxGH8mY@I(#U*%IVn1`zG47b~p3wvj?WWTVK1nv#RRxk^ToNDq7_dX{BZE zoz~_-#0X0Lz9S6{R|f)RC>9Ex92zLWiySggH|T@>GdIeFdDhR1_DV=dkmv9%Of-R(sV=iaL7y-pl1cJ8UF+Us-`8Nl(|6p3!M zSX@qsUdBo{SA4pi9L}khAQBT=*xE(P0UjyMXpdk?i5m8pj(^NlD=T9n*IQi;8@H-|dsJ zS*N2`Mn!5XejIMvVKkX6CeyCg&iz)a4dqrjoSW9n%=B+Z{?l8x{!d$NZ+CU=(__64 zRaJF?HPB#Bn|rgPX*K#)WiWVUZNu?Us0yLFX!Pu8I6SJ=nSd71jpQvLJ0HTb6Z}D* zCq(hdTyEBc^F{F1z__dA+Ot+v4u=u|2T^eW9j&=3}h@^f=@eQIS$ zDRoPj-!{TL+Q{S7jl6FpWM>LreRU-GKbfnR%x|(bDI`j{SW;;)R>~v=Fi)@9G+7jy zf|R9HEQ;s~yh5?eV$iQGDjL)49ppMvnkSAKv=OmbrdCMHG;~)-ZeF%Wukk}fJ})oV zs|j~2lnSL%(N$zBmE;$ur7QEq#o9oJQjXzmKBY4&PneUP>s9G|Ia!bnB9W7Pj5-B$ zJ(k4IOm`YL#L5i06CA4G5B< zwH9+kBodLQ%IEF&fI+Pp3OL$S5>qz0^7s1Ee34kEEbOq|k>)HYtR`Qy_c~E=Y*ME+ z6c!Y$F5>I`Ik`E~d`Y>v1W6T&o>G4sEDK3RU?nPpML9W{&O##|gRYjla&q(V>a^se z)LB>^>#3D-VR%k(2D!u(r;3+FQ$W5hnMDQzm`b2|=NlV$Sj;wrY!=I0W5b-;V&9;E zkfz_?s*o$#Pk~msTtRQ&UcJk1_xT+5T~*twcjJ#>&VF;dd$n3+(5uwlZuqED>-8$t zYHSCATTfrbdlY!VjXx*&0sW^W4~7RaA~e2=dG8JCFf;eOszf4I=v3++Z_%JeV<3k~ ziAWUI75apE`J$X0n}k_lo*^?j;-8RzBELJ*RvJGS7qvc85K*Zr46a6*Osy73Zq}D5YECyS(~@5_WdvXyBHA|2&T|;%`FnXhze-mwl`177aaf}- z6^mqmONUN3={a*^UB#GvqL#lA+OKC>9B2E7D{+B7dQ~K?*T6 zsjSW^w`Rdsg4t^@_A~5)v&2{amM~OD(r%uhhxZJ;fE9j3z7EiXaD2 zWAny@qAYZ2&lPr?L#NeDR=PX2+MqDUSzs7v@@(b#`T3Bl=qo?lg+v_FX~JSf;aasqUTg9WXf>9iDn~RR)VYLt64Gi` z*Xi_Sado!CYOS_vlI)x$jgu=@APClcZ-zNM z^=eb|a2mfGHAG^kyV0JWdt;$Ggy;(b3Smhl2io{1&_)ILHQPTI^$N1v%9VpGkqbLN zJjJgj7h$TI1{Q5X@PggVX;;MRTY=yrRmjaY)4uNTuwG%y%Cac*BPI4mmCBoM7FsZQ zn4@Bec4hhs^eIy7^xHc8Jz9k|JIkWfk3_l-nk>e7u~gPx;;xel6)RS(NJ~r8$i*>p zq)jFllmGJ9iABIQ%-$7(G_5p0M9w|l_i#l?vrZ+iGU_X38a>L*$};A6=BP3WIkDI$qhNc2(VX4P^ zHMzFPVqI(RNYgaw9i2+465DsH0l%6249Ltt@T4qg#C(Or$^tRQ?oxbpnTf}S8%dO+ zeiNL>nRtLlO7WKWH+2uLHMATJ|JHlHZG$-c*qWI~N~4uf_jLc-#)E-S4}@9wGq*P% za5%g!r{l8DZQnK{kR3u%%wihjWp1nA7adklDeIj3>yEbm_)twteR;)GGix6yFK;yK zja#ZBSJu_Mjan|e81?&(HntxR_)AAxvlLl%;jP|Kzp6lkSNA+4hm!zm zRv;txb$!&(jb>gAOO0`-XND3R)r!DVBhE`fSa`tIr&0V|1`g z2iGyt>i{30F#@s?{h6t_k?gqe3L3=szO?u!It(jq!*K}KAOk*vFW7td60?pX=ByNp zhNY_I?jx|NkrgDVs1oI%BGgc>UhUI|bF$L~0&Q-t962?bHjT!iDJ<;vY3nqqVzF9U zV>WdeYxX!D0S8>wY8*%}&NqO2zpJYIa=XJt_iwyLEXm5qC^Bhk^tM)$xyv0}V=~w1 z^j&^WtFl0qxoV{{Ya*{8J$7DV8G#AS5*h8WJ_6b=#Ie)cjNti z3V5Xr;;ssJ=4x@L%KD+n0=+Ls%3!(hTnWeLES;OCxmzE(M1;>;vXX*>;kwQz#C#P z1Ucc(az-NDYxHY5=H>(ex7pN-UYm7WS;Zcwvlv1!6`{~gt0&s7^m@b5VDS3Rnt7Ye zjqErqR5fcfT3|@X?>N{x*?hUj6Dli$A(itss|QV$O=z`xgW0sNXZjSxVEEs9UI67# zhr9bPbGxgQlAumES`i!9>I`1HV{gOM-NoLB;I-ETMK1T@u7SgDPnAL(((6ZHwf8Kp zqzya+u4Uyi;md<~T<*8!z>EVrf5rZ{v8U?lM$v2^9+q@<`o-(mz@VjtmVkey&_Lqr?ii~oXOB7lMnEdtSr|B~i8beb7}?_=nd zE17*-O3UoK61~h^i%v3Q=p?G{d98>1b9_r>C3!5K3%|gVPeEk30rK5WSh-(qS^XZ1j$L=RLoeb%C>e{S0D5ELg>vm5UGPinOokX%+xg) zS`RwxZiFBizQ1%xrBb6+>1#5woOZ|I?)_^nyk9Y;Q53Gq7|+b~I2}j2Y31VL;;&%6 zP|M0U;JPG2K(1#_GEgH9HRK>CVV`ik7-kDd;DlDfR9u!15yQOM?=NhZk;-H4;GqfT zCIM{OWO?a?K?f|sZrQ-ljXr;n|JalKXP=`JyY?H{HR*~Kd_<#P^qlSe$>;X1JPiBx zlissE8<+M?TB6oA=r>|Hbv?+bhhXRI1k}5i`L&>Y5@ilp19E-{gzH#|jjbo+lI7#4 zP|mVQEq(JdJ2ne~cE^FX3)>HOT?)#Yh6*FGOxhe&$MUmttRj_l)vA>!Zay3xD8PRT z2TB3!NW}c9^$Or~1T}7TsMPPjwlf|rpTV|r=Bj>bg-Rx?GLSDZ?~=D5C*_$z>$$9JU-YigvAg2)B|*>8uCAk=U^wXW9P8=6+T$zUac$4d zO^*$Zexv!Wo|DB@mBlBz?`V2%bm+;(8@i82>+8`%&qGxujSVH0kMwk(EiY?tjg_Ax zNRnFol*Cx4z%ODe4pQ^+YRC4%qGyc(0d5 z26!^?m8w?1B$Cc4>t(Z4%&%OPVO6{OwOVV?=e@3f_4N={g*q*G|5S0=luo19DwOpG zM+ub4OC7Kv7fcG>V681wg$v9>0sqdbt|M-@&*OAn))5&7&NJw=VaggUsQH9c{u#Xpo4%BwS z-2GsWUe*?d41&g>)2t~enY24POvW;;whJ8roNKgh*fRtr4DXV?#JAYY6Q>@*79e}~ zBdfi!qE>tBiI$@=XW2spA`2J)L4HU)i}Pq}muf;_cgaGik_r{MoPpKmP{?czmn<%l zU5o#qG_;8}5F=34EFeaR0JQ<&+046C0l1QYrVDqXbMPA?#pG`CMM}eF@lxL|#>>ZOAb*HgrQ=rh0I8q~sR!5y%d$O4ja#7y z6cS0L*-`8)1itbS*+acWK98?}d=*h|#bspAg&>V8uYw$gJJy%}ca#_q**l4y5;{BhYTd8e)3GDxdMX0QWo!@}7qHtv+T|sT< zgGEHlT!P^<#8YscM7@g{l#ci{yajYSeh=^+&@Sew)L%&Jyu((b^5ou`h=&OC zdX92$uQ%C;9ZR2$_F;J667KnY0?J6?{Rv#d?~uQS)$L;})DV9ZvPzH$Z{2G{YaXkQPU+iCyPBGxo}Byz%%L0RP?VShN7DG^*yH8Cl0NJb zM&Oh{5mG6Xt?VRx{RT_5L?}TB!bee)X-!BO+wO%~C{$!=*8I=bPCePuw9C{CN!$9z zCc3`2A#eOU{UZ<5*Uy=o%{!Z$o}Qe*v-l44DpkNg0DFRI9*iq(4pqS1gZe(;OD8|A zo#g)!wIsQ391Hegp~Yo0aZM6O zQr!L~Jd6aU6Wh;;dJgBPA+Pta!|75NYB$t2ZqmvXC=j&m@64$fD$r?=-KuP`+Z&Wt zJ5sCEYnX4fX114zQ|vaXo28hnYh{22R(ZJBYiIHiIpR8~&UliQuwZL}S&Rb>e)7(FrWrjtkDeuehhAq~ zbBq?fHwWYIBOjzbyePe>4;Q}0$~DNgfvxGH{|-+|*bqz4qChf|_swFHdKO)-UicPO zik@H^CYgHlzSfFn&|n*L7aD3~zK&({R^n~Cm%a`@W#PIFERRu&&Pdg_bnk);ov|4VcDun~ zLrWjXOLy!*-%>lBYEUW*FRGkQRl-hr5M4`KsGH!~9My{u)&rJ>L)0GlwoOknYp17S z6^{`2(sp{55VG&b*={iX01p&=gULtR8R7k?ocY&eba(uJ&=M#LiKcv`Us>Pu9ssa9A@)is>us7hWg4YH>TOfj?JQgi5&Mql_Gp(yr30esfEC|E{FYjzOK^41T0b%dksor3i*bnWllLs#``ZIZ zA`~KsmK`3$yZjXK0aSaJ;?Z+3QjEj`^c1yz;YLKBDT~FsKR~<3(5}7ApQK{u4}gU? z2pesouY&QcCej;&dU!sE{CJ<&i{C+2Eqn_d-MWyI%WPrJ^~4*2pm1HQg>Inl(2dR&#G*%C@96^1&@HY84 zI*2ZxX5g4DI1!7NQn;1{TLi#`=n8UpQ~cCkzWuYe;3|TvZ*W)P^bPz@j=)vk!=Jsy zx8u8g#5w9Z;1swU&NLu&n9X^dOXP8QgbgB8C)r9LFVRFyVhz;sC7~J4#a@<&OB}Q0 ztWA_^1A#;{fVNn=ht-%%p-sbV)o>Ji&q5VK@MAa{1wT|Zk}DL3EtW7G zS3(F3fH0VS!n|Bk#14-Rg2B9Wqe?vjN1k%SR%?k+n1jb!Ebu9p86!KI;t$YwHU+0l zW{<~go(ePt*Fl^DKCU~|q^m1X>U2tFoxVv|t5E9UV{P2l9Gv1tOa+?*$-&LKI;f?> zBlOMs+Qi2?I_kXe0!YVD0*7umQMZ=ZPuxP>4m_EPkszxyIV*h`b_A@@=P>ovI7FO^ z_!1oc-&|pHlJ`rbveu9$n4cp{PnYIE8amV>lY&hu1!y2YSFv)XQYiAPSSWK0QeACD z7e|Czr7{V6Gd*LKU8}QatV(A;Y3-{r)0yMx85uUM)|Ls-RT*}T)}E1(PCkR7I{Y~e zpm5!uk(nO9j9ga~zY%q|xyDUquP`T5n@2NFq|eOG@tMqP zC{wEoLX%#|#jkO-;sHJYk1rV83M!Neqd}>xC}>kwDwGC;Qc+3mXjN8n;7Vm{K?Mh{ zSa|tMM~A6hzJ<%diuEy%lKA`*wzNS{k+)7TkMfz%NU#Y>74tE96MGFyXD~K`c!X*W z{xBa?^moXA#?!I%nF9^g1>>;w43^81wxhe4mN~49=BPK;p&iW8bqh}F4H&-<>TLUA zJV^6V*3wzJhtq6yKU0aGWh&>HZge01o}Z+qr|<)&7H*hiM$jpkZ8y;2VY(Hhcmu9( zuZR&CSu-?2z$rIVt6RwIoZN`R7R!_7=H%s-yBttYo=vvM z6w1L^<%qISi5WE#9aJi0@!CkZzAR#bfdz}yQ}^=+*U z)z8oEezmf?9=%i;$`{L!R;lYSR_dBmDzj9SA1rjVt5k-d#k|f}5!ht4xQuF5=QTa& zs;avhBh^n2jyzjk*3?~B@mSyb?>D!Pch|T6bkodREp=T(O)am&mJpqQqf6-qIAi04 zH~>`kc6`Ok#9x64y;q2fiv0noI|K8@4{;zam?G$Gs;+!_&;C~{E9%-BDqon}@qA@d zdrd_-oUog`r=+5?G+c6ScIIq28bh7eU*363c@3Ih_oLQ*4OO|>IqpJIG4rG5rv9;} zrteSB{-n94tFxx%zcvj$Ra4s75UGAFl5wKw~iTbP}9)bWB) z7%qkC)CVA$aSzjn-ma=*Z0K$50d1w;rg|Y}g0Tg0acmWQkp{jBQ)sJEt?Bi+d!e?% zpjP*|J-r&Wu~#a}3#rv`k_OVaq9T*R`^+pt7you1B{q>yB;3n1Uz}eG5EC&YpasASdtF3cc{HrPTPFwWPcN z4+_;3`QOdsO(0}Eyo|LsF)?4R_4kbhHAM?1vRtlWA$?ui(QnP7t&aq z9V)FEeZ736(PV?Z2;yqH9$g}j>CF865-q7U^PnisMeAvF;vD)nDwW|%C4(}G`9>~d0M)hrGQ>#J6 zNDIBWqI}wFZ!av=6-Z>|`oa>q!ka&%tJ7=12O&YXenMXhbtXeNt6df#m+T5sz|T!m zVW%a<%3Tuv))hy7+R`z^=LKuZ zetNsQ$)K;2%cUh6b!FkDPZmX7JYH9A-7}LL?(ljYG$}633!4fI>FEN>S1gl7%9Se%gzed&vsbQeYXANIyWnJQ?_gJR^Lv+<-&bn&tK_nB4f<5? zuP-Zma82Y;S)fh<=}uK)kyu(#8Ep7oX8qTS+>yK-rY~JxqtuxFdJ`a)B-S%I^eAxp zdg4mJGb`Z%)yc0^1LVt&HgFCqi&xkc?n{tj>wQ^Plf^bJ5ofWcT zez&H}<(l%@R~t-KQmw{scU@Z7au8zdRw=xkZ6QC$c}VysO{I35vnYx_N+ zy+zS&Hct@>x!n7z>W}#SF&N)bRrSQ0VHn?_DUj8gENv=_JN}7l$ZD!p7$LLnHtKq8 zWow*8Bf63x95X9yx2ej}LoM4Zg>c}ZFf&7zm2Hp|H2X^iG%5|$R`0BxJXI7Zw%hH8 zyJKSpr89SxL9Sa9ZQX8Fd-BBE9HCjM>IJK|P^B`$xtuO?)7K$M4MBv%eUVYU%A1$2 zmFm`@4Vj9ZY`sL)>i0KGB~rdfk(FzbYJ1E4EpmyRzY@ebBA{@cdJ1G#HdqND+S$}< zLO6J-==`r|pD2sfRF}n`oP`E9Rgu#B*R4ATh4P>o5b;m)1oaNw3;s|CcPblc|GD)y zQpxYOG0*mqCrEE&WBi8{`<@bXFMTb1gaB@tk=@%wKaCV~H4*sX7>(OQPB+lk4kMXZ zT5Z>qD&#)7wpMRwcR($;-hhTctVpUIg+W6v*Xea8@{j4Z_5wIXBFxS* ziwpdD!hE02wmq6=Z!IX$Wo2a9q$>*FIZKn@CwGD3j*!NqGx!W~KY zFp-5eCgH_N_yAFknv(F`Bz%-ul>#>=;eCteF2ecelkg#8C8~!xOfX~g)qq;#a2oM^oS$6HY;tyEu=*`a@D=H#>eA^aMP-P#0pA7HV|_7W0=VLY9HUy4RxJO^)2!p8|YT94sO z9qv7pfb-FWejz!!B{_P4XhvfIXM6kzA^?Y&H0ycwue>~DMO2(%qCP+7GuV{XK>$RJI&{)?^k`0*Bz6WpDv$)_V*4)P8f`em9 zCAP2#zKn#4T2f5N1pmbadVh6u+L_v4fn= z6XgcYdG5RnnOj~rsZpz-F+)B5FdurzJbj^Q$Tg@fFs2E{)6zq3*WM~DTfPVMk2@ln z;4KO7g)8E?=mamLe<(uDS!$O=ZdC^J;mrPW`d0$|Z@Ab;N#vGk8lZqImHMlda#tR1 zu=M{xBkwjfccI?q*0+;1vgKiatehlcrNs|!{eT?r?)%%3V^@9FwR(7oM*7NIzQ6g} z&EIdXU7``TW0Rl)_5@@rQ0_31i!Oz|;^3IlIrsok%~JXfrW|*8O2VgzUM3DwLYa04Zp9AlpU zXmh#lNJ$G4*ork;UJqz%li5@V>?47vU`Ra`2(WKeN>PBG47?0|j)II2HZx zq^g=FJ+?^{x=-QHFVHS0Djg72A<|!LAC>2jX`4*TsLEJfAamxieWjA?!xEC4*C4Tn zm5{jK)K3L^fP;Sz_!~f1q9PdoJ?1d(5(RLGAp(4gC|-;K{O5r05dWV5&*1RAC`XDV zI6Iz&L+0u1;@uby9SERfRC4?jQM9=7i{sG<;f#MB_z%v4(=C7}cnYQ)0UX-E$bbOm z`uV4yrUv73h{OlUo$=AoOej9Eo*WsN!Srz#+-GO$0OIH@9gMSdz`?N;w;MY0Gx>4Suxcg~+~0WQ*&QhkYf0jK6Jrsrx%^^^@T92hk#K z^6xK=z0pw0eYi_|_ht>ZqL0y-jBS~ykC9yI{+=ONT{u)sGq(B95h_bQN z*n+B(@L&=?4$`Ux!-3kNw^b58MJQQnXZudER1L;5`_7 zrz!ylJE;6$@N{l8rgd)g08s(7d$H>ia8g2-idZuTx<658m<#9)+yM%*#@nfiNpxcU zdS-kQ(p(krM7Cd)zza_lvGW=yR7vJ5zCo^q08v-}b4hK=spVtr~No zFt5z_@48lz&p<_%^I%(ONTan167!D;qRDx#Ar4=hKmQ*|_z>vtN<4q&DD4KRimSuE zyt5Xr3j0!xlnBpok(@7EP%IoeJ(Zr9Ce&nP=fVlNm>z6@F~q|lZ!3jEEvk|cwc7B- z<*j+7yrKoNJh?`ZET~+jx>O;V(vz@Im4fY6m!^ zr|DZ+T_8zN$u^dHI5^h*9DIO?vO1fCW8KfeM?v;rn}CC3Sjqv;lmI`Hw z=l8Qz$iXqSaqt1c!0rkM$JEBbM;HH=0>{+G!TT1kVd0m+^L6a=AtTFj@?6{XTk1qz-f*)$R`nN{~d+>|LZz-S{0KO7(Pdeg%Vkn zuHb*yj%)>UDAA708C+^dmLv8RXj{$8=v5n&7y9%67mdpFb$YH**=o5~xzwm^35R@V z5{F~i?j-cvOHgw9&!HtK?=K0c0DRt)gc#uL*(!F|>$qLtgm<0o7{>bzj4nvf)IlOW z3BQnl^U=6Wh@s%`LVvL&bRvlTUG5p5C7}aNc;63zY@A|ijtbugY9(gKv+xFzI1ZM} z6JWXM*^a$_EfgBz4A4c3g}*Fc199V=WjVlQy{70Vo}b4}JH!#KZEa}gPJhrB4*O56 z^^I9gl`?u$JV$HP9~koV=}o0VVX4vB@9Ey7vm5C@PCWeZ1oNCWB$CPdph1a;Yr8+%7GVW~xe5 z^|J<(8A@$zR}9r`cj$`p)27K^!x1yR4bGnKsfp(W22@IQnqXa8y4UJB*i3#jcDlGY zX4EL^ENR~aI=GQJ#P(n-#b=wjOaZ5CzQUshO#>cwqI?-D&{J}BBJQBpQh&A^>&jzK zZp<#sa+iGGJ<2ard%Kiu@3!L}e+Z@aZbR=wCmE9~(YOsQZg`=#wv(hN`&eKZSAzp3 zc8R2QX584VDpYcNQ$b2&H}tKr+j(h4I@i(-c7^S=#`_Rh%WL=rTCaDGVgYA!nGM&%gTWd`_vw zF0A9)aGv9z_Sa?6w1Yp|c;Qva%VYTky5|j_#GCP`&2FCN3puC?Yp`(tA(7z_3 zeLoT<$AYX&JYmQ#1R8n|vX3Minwav?=ya5vME>W^)n?9IaF%R}PfuBL*n}iD1@g#I{`-Cq7_+}2D#C?Pb z;O{a=aA!5Zvw?+AEq=0yEnXI%f(-zN-Dh?FrRZ9KKLPmf=TlJ8X?%y3uITo~Pw{xB zovvh`!;PN;&k9o(bS#(9 z0oBddP-rXHr44n`)KySN8-Hbfc2|7w^fdX{%q%?pMVO6*)goq?C0GtV1~H z_-9y$ya+xA^hirUaRi}^^*OlFKvC>ythWvlm>$?(XY8{r+_Tr=XLC@@RUC8}VXoq! zn5#JGfDf;ogT9f3j_QAvR5G}Y%P4`vSe{Fq4gzQM zOA3IN6%B2_*k%pc?cN65Gkz9j3$wgMk<*j^i>7M>_f@ZQ=}qg(Jk3h6c4bK&^kVy} z25d_!#{8DR!f91Px{c}Hs~ey9NP=C46hlIRVhAmqL{}R#Lo57dXtp{ z;6pEWpdn3i2#pO+4|cg`Vr+jo=*ZaW9U82+j`+C+yKs{^bFGRwP!c77!u6T+bVDLm z>lcdhq1oKgKQ#R8A(@hEH&=gEqO!cBR?lqKh6E&L_+*dea#oK#E+6}1-6EcTe$ z)$ra2{wm_Ar9Ev4A6Qzk_`tMvC8jN|pBhVBc5ZYn&=zt5HG6rOZaL`quYqonh3Ue* z*Ah^m+r`Tlv*Fp#0mazZXTZ@*~(fH#-=m$BX@%bZy7v$;erq`fRF%=R$DL9s+1v)}!W>tv=XD^U9Lg*kTU`zT9v zGUI`lz0}Q&IX}sKmuqN7(-~>~lz^hRhGr-s2if^8hvS@6Ziad$!(Sl_-pwWcdm zD!ohV%H-onFSo9-zZ#SKf~6w|9o&WO&M0uk33k6h-{SqA#rw@c@y>G4fiT7l2gN(f zK}UHPp?GIGXy4r=jAc8-vCs8!&z;B5<)HYv9CV-(KY@ed=W@`|%!^R`Tn-BVr2~w; z0p?rJKG)AZcRPMA2mKhva?o)Vw%|D^mW~{BL`6Y{=@t47O2GRW$VsA%(h4cTBiFo3 zzk$-3f5JYam*DEkrK?^cx$Eii-|~LO{1c_)s~Y-`R3q;roEs@KY6T)M_%c-E-x@#K zaPOH}df{(>$$RiYOfA>bpHLd!yWkNebJ6TMVNTj6ve2pdXG+sdW@luYq^7b$Ri(vR zDUm8r3#nJ;S*1#6p*(D~R7s?A-uJVlw(OidjVKSwFBJ9WRl4AcG>XSp!a-`aQr-Y_ z6!V&?Cm{la*9W;5>dE*huXzljbf}tsp1Pd(0(<_4iz;(Wwyehqb@>E7^;2QBRf;8Y zG=ZzX*G1XR_q-S2kVd`)<_&E#hT*>_(Wy1V_yi3vn|v1fw?JM2>aDOKV^6q|Sq>BY zF9Qy<$-x`bI-Od4xQawUo|d{6DvDM@g;rubSpI++UP?7O3c_o->hmX-KJm7(rRVUV zL@)b^i5B9_Yw@#-TQH8RQt-6Ez$q`M(`0o1dI`e_y~i|BK(07;G^zzh5H^gf zYe^7Z;b}g=6d=GHF zi?|#9MZ}L75ab}MA^ySkg;+`!;K0K&?P5e#b92@!drQa7nqVG(W`2c1ZP?ycIi^w< zBD(mzfy^qYDNsntO=mz7+o8V3;&i*5N4oL*KY(U*tr8=ww4^uuZRA{D`h$c@<+5CWn$9ykDDYspcSZj;az#W!)81+vYuX``(70 zgI22`x)8y{$b^?{%^@h{NA zAE0uG;j3_F0CX6B8V zQL5DZTH(%yLj}1xE``FKn=68o1s;XMfb)iZ{K?G8TvI8 zWx(}9@fP}rOdo$0chxJ5|LVHlNBOIm7f}@GcVRRA$s)mfmj>JU!Y?r0#@p#Pnf?4e z_H@wl-Vp*Wyp)f*T@c^ir;19Yg+aS{!Ye4KE+{ZVV{J*efPOPd!)${lPi3Ak8#N2k z({+VPZCd&YiXbm+r+>p_@&3Fd6}2E0wZgc9`S=#(KpWmYcAowXR>G_zMr1i}1tsUV zFU_62d3D`2{C4I^NU;+XYUU}Zm-#2)DtDF%yqnasObLn&V68&#*N(I>6?UW14*e=@ z=tuYy0;y@^pIBI6-qN_;8jZ)JN&E|#Azg5)_P^nzE}s~OLzIYk4p<2|jXQUWs~TwY z!VUAhI_P!9s{<~^cF{SOi?LmR)M;c;jL6#fzvvkzjWMFj;oQM4j#JM+MGv;LUN{Xi zXY->_y+{E~LiG~X)jaHwF8O53{yT?dS>D89O0J5HYG)+m@5tg<>0^6$Jz7!`A<5Ec z$>V$XJQ<2bc92$|{j%XTM=TyV#^teHJ~niP+3z4vPfXZe*!uBM|Hkz_YyLX-!uG!n z_sq`Xd~Wcik+<5aRu5FS{dn-Dfgg5M4z4cidJ|)z8CHqw#UQkRbs7|mku&J--Hc=t zBi+p-F2Mg~b-oo=bIbqX?YraKIDND6{YVM~@5Oz` zdCxx2!F9k|z^ejAS}lT~klgpZoV%C5LVb1lhn#&=Q&ZH__GuEruNzC)tAp@UIWg=iQe85PM4Qc=XJX9`Z|}(%l*~cZ}a+^TVC5c`&x5zFYhZ~ z;r6#Qzc#z~)#he^<&E4E2{-VTpW`-!!{^3E?hQv8xSSqtLnv|&GuNvy=1mwgI0=&- zksXZ*iSb!!&a^nlvO@-bIxB*yk1V7Y%oZD2BqSaI%>M#Fe#6N{G&KN<$9FmJFWU=& zpLyFL<;{G3_NAuA-a?>y9N5s?*!1!&^WNa?p->Z^3*R5Q)@2bfb}(zpx<}U1WzLrm zU_K4|_BwJz5h}wZsLzmR=x?O<>h*d;8tQ6aT)g_#+E~J1(9fpm_sBB?hxIBYicuZ6 zZ5zAA?(w5}n||^1=_&6m1O2Z|Y`ywlhX=RqnHc=x($e6rT*}m;l?c>Lp_`~(6k2no~xGRgPsVgX0bBfkS9#&0B(<&^+`V0b8gIvxY z0QmYpbxW!0gx@BIDy`mFRT=`c%)7Vzf@77G!>T=W=v{>c78Ct z*<_6H%e`95bfWKo-l*Y5pD8dmNmbfSJ)Q6D5l+9EY8z51#I+_}twgCQE}h`;6cS0T z_K(8;k2Kfym^7LpH+@JdDz~-Eb>^06;z7*u4f+7L2m4}V&LN9WBm@<#P6*yCHUXNY zi|lI-oD10tF6Yi+Wpem#EJtTIYs=_8p>W-js>|Wn?Ar8rBGD9d8@5C&O;W8s5e(hE zapIQNLw36t8Nf$V!D+j#QRpxk_te+yLU%#4!MKOV#|Lim`9hibY2sqKN2M1mR+~sDOqw{ZS~c+w8W;}%Jf5hEvI~en#!=xck4(8TEc`0@wweMj*Ojk zdwje?Z=VJL1H~1CH8l?osrJ6!($d@8((?LV)%hPbJ6fs*UadT)GP+BO(d$K7J!-aS zwYsV0r&ay;#iI4xqRwdarZ&~!z0qi6V>EJ}S-bnN0s}Y;z3}_3(Lh41cO~ak$vXQm zZy*TgEI=f$uX+G6W#8Hp#4clY74K`0rY>w6da|)*hDosHNW8kMZYV(2h_ z``gB}PL~j?wGNx(SbKEA?M{+I+@6}+hx^B$u1hoS{@my%@QC-NEz;rsSCX7e@Xv=)4+x1=2mZrs)HU zvq0`^c7Tc6y?lt22hG-ft*IHEPJ4)HEpXFB%c2|jglI1E{m>R5h}z7SrS!#@xUEwU zqJfP;tLSv`85Hc#fhNjeaZ8Ys2>Hiia02I%Spn7~kwbwDWt|j&SIkxIucZlK?Cu)B z+1pZE-|W3*oOxB>(CWQ;ym3yt)heWP zA(!mZ2V9ujSxXI+$Yv6+(!uIh1vuQ%W$wd^qY9{L2~pHAmQCVv+03Y zGPNXYFu7;rQ|Ey6wS-PTyMpz>##gWsC+XR2?>EM7XUk@CM3JR~f-Iw3 zp#zk5W=R)6*Cm1GSUI(8{!__hU8=F+^#xm6E>ojjZgrKrI-+k;$Wfs0L`US>iF3hV z+~IWGFy6L^1})?u1vvYMxSa^oytUgp`+QyfP;~*ntlX|KBt+G!;?7Elj~-_XwLi*Y zekvHedC=PTKr)d+z1+^)v0Hqe09JbrC;B+{Q=Y{S324^NBxfGycrsxNsB*_{t=Q!r z&wPE~g+#KhE|K`ef&I@X616(5qSv2!oVc8hlY@h&oDLU3K+bV;c;uABfxVyZZ)tsf zmwNW|>D2H@Tia)MXxh(2eFG{wrtUizLuZ<3>|EcPCP!HBSD;72SHXC;7;7xYeV7ym zNRG>dCxH2PJK0P0dfiN`$XQ+O5J}~Y21kkw{{qtK+MCy6$YOI~+Mi|NiU%T6=cG3v*33L+^LeTJ9)#s*}O_ zZvu(05rjGZFJQ%C_7BWdL0l}Yg#>H$0#;oNEF5?&;Pv^vjw3zJRF}aabDs@*B6*u~uE;Og_O*Sz2f~V*v?l!C-V&Pap zmgHd;u{z4ysp)V?Sh-IQb*BhbE~i9%yKI zW*hl6ExPL6uAZGm^Zw9j`OfM~;=zpTIX(J3zf{OLua8FX)NE zSie=FvSyLtV9)I<*7dT+g(7Emgez|bf|Vq8jm%t9QY6-DW(UJVYO$^aj8rugj*Y1G z_L8CvB_*XM>DBHy;T06fgo2>ngf3JPAT^Uk9TSwmR z>7KS36=|C-tuWfWR`f^-9q^@ZZ)`acJxTxeR{M5$V6#qRGHBGpK64kkj?@3>NSVBY zO0}-1w&}5!TM3enDR@_Z!@Z054d@VQ%$=c_HFVfUnE^ z60#q_y{pL3X-2lX>2P23UItH;bTl@+xJ$1$>}yZ&HyO=9Q;adKjJMwx@P~=5FLMTA zhz3WbQ!Enpo%Q*m)T7&r%PBJ2eQPjS3!K!$O@dv|Bx~D?{;B&KJ9vZF>9{&g+9~=o zTOW_b8H_a*gK+ zh>dN)!`N7i*fuh!hLe8TV0@|Cr&6UIa-URGrmR$Xt11;GM7&6ZMGOwRTA(bemiXk( zluG5}{v$m!GebTW7y!H?DK6i%p#ZtGRX$1P&df)tA!?7JVuGdxs4mOX zVILmHdA2bfZW&JH%coW)LvE$7NQP|R5{$Bd-5tmZG`L+)gNy({HL|9G8|+r2qTOciR2a;)0pmuGXRE#bL{0tC zSmu(h*KBA}m_0r3bp*KKM%* zzQFMAEGx)K5_W=6=)lKWQNwUOKIae1wPfSGXKN(AS8vi2K)7sgAR}kpzM$Xlzkg?N z(hVQq)L&kpzQ|Yk9qyCu_2@?JrM6OWf9U+gwvQ!~&2j-%g{L#9oD~T9BLi34?f#O5!j^dQsXYguk2N)s z+IalM!+V}i#9PQK{U_me8g6QRWy|PPYR4B_YSVs~>-wIZPbU%$W9i}FTuuMi(hmm* zCYzFprUBC(e_NM^gWoy>V^dWD8j`~MhRF2GoqQVX0 za=t?@QTV%6Qd4P31((Z#8=XVAUGpY`US~i|<X&b$6YM$J)pgi94P9g5i0mGm7!wfbrje@r#+A zMn^_$^tv&^A%@BWvexfWpu6E9+4aug5wqK3_gWVQ#t+hW9i)23qg@i6s7fp8PK;9@ z&-9a+C-zwX=wo4L5Yd**HtKc>JSN^!votaRdPyr`>k6lYT$z)+W(F@Z9E2i0g!rwx7A)ZZ_|0 zL=$rs3w;a5IE*J&~lDYpiEhL5Ijzfa-F|lNXh)>$}q*>u-Qd zlrXj%Q?16tgcvV0+|XGFT>OvU7+Zn*HPPKlzeub%T*@0fI6_~#fmrC5ZYA2TgVeK` zUh2El`(9lCIQc17(x>kT}`(rva zg3*>S&kHZ0>8>3<5JfT&zPqpMM!&y?B6rR_9DtJV^F6q;>#)P%tElj5Z3mmC&UwA= zkk5D5rsi3b(Skg#*|x}-MrJ4}Etw`IWn~VHW+a+Eh>CtE`p>js&L78WYOtD!w$>4B zq>nHaJ0H)?Q490bS!w~H#aiwgsG)u9Vgi@p4iD}?iAOfJdU@Ngz&d=`#wqj2|=JURB` zvv?As*o!NHv@5h3JR?py((obHN~qXP^j(c9de04%HCX9Xs=B<&S`lYMh23b_8sNUL z{LAx?aw;x}ex; zU%>oJaR&lJ&_7ocH^wu+s?GC#+|q@Pdk0dxwR#PdPQTZ8&yLZ1Qp}7yBx<|ecI_a! zgWj|EBav`aEzay>*7Y<#mx%;3BgwKb33 z%hvN~FC6ltgWfm4*~Yl_b$Y{|6j0nCA4xJCx@TnM?qD!MP_L%jtAD;pU>Qp397%Z``a>%4JH`md5i; zSt$C<)MdNP-xUhgkmWBi>qC8mc!Aj-PwcbVk@7?3eGShJWuL9WelZKQcBZ`boRKsx zl1CWESYMxIjNc~KPKGhMY<4ilZ|5GmhBHpBVT&a17<7O}RtM-~jE@zXUElm_E!PkU zp{T4ibN{r+Ye>YO+I`?cO;a;zZmN0iz^x zf3?l-CSTn7T(Y)#sB_>~*AT(g|JI)#ZmX?(c6;v`k0+E$wtap_|K!-@=UQu0A&&>* zlkGLU8Jte@*L^aD3>ATmt~F^mYop~W?P0Pq1&%C7#CCjm<4Lp2a(-?Ot*Y?D ze6~fDyNo64hvX`eNG%zzj}A!fYAQtP=YGAhF+G|Z{K@PN>V9n5GYflvHjo+}Y#aJH z`4s)?*xOC9+LmPNtHY#m_|?`(Dqv526Jth20k;@CDPXeekqwY@Bd~TKlVX!Y3Wxa1 zD(jp(mgte)pRB2EcGy&7?ew$9NR*t-?C`o>*Y^__eWBwjtIf%~5DkSMnoM8q)z>OB zhp8RpVW#)2J#&H}&EWu^dkVYSkk5yw2vJ1V3sb<#0`@1X?J<%s`*o!-0~bpo-LEt^ zb$2&4zp`6B{d`?rYAWvc-`J(@yE`0iKoIioK6TfbfIrTh*_W}}3fQfW1lVDpE+KckA~xo1gV>MWxiw%@GAsU^cYFfZ;~fC@6v~+Kb1<&QFJvuUv}I;>WhN=o$-b)Ro=!Jt)}jHdZ6ZwETuiNunQkgcX# zAi>eu$ry~4;udeXONvI`3i-H?nel5_%L=SzRzG1l6JvFA&Sf4zNBV6`@Y?`j7Mlx@ zSzLtc8Gg@14w(#r%i1B7!ea9MhtbXc$LHzJHxC^mzoYrtW!8`%qV3J^aA!U4EWn-6 zHeeO49A{lYPR)ZF3rICc_JT}ZF`;-oVWqeuY9h9gMT1;hn9qZ~^Ufh|l-4=6dxG1H z{wUygTWUR5w}cJ`$ow)K2_)}sJVVNkZU@v>_z$x4(NW{{x6UmPV;@n^-}*S`Zp^3w z<kdg zS*}0*O8NvV+;Tpk6VV_%IE&jK2nC{%U?}@C7>X z2BuzaYnv#hXUT0vo7!4mnNm-_($==An0$s#6>n-yy>d$3aeFM*Q?h(jNp~!E$6j?; z_E*lSlAc)Xc4jTUL%+_w2DAK-hW8zgb8q3^rOb=WiqNlL)@NsLhGsce_q>C72c$%$hJbSBEi96(h&{vYdnU zjjRj3uu*`5zm1jr@ALDoaUFKsbz>V(+8tJd*?z;0p=+$R0B1p}k`C9mjLD@ULZk}W zNPW|&R4Jx9$#1Y}znFNjt!Zd1)%B(A>WSCd`=+{U>s~uhr%pdvA8YTcuX&(N-Ojx1 zPS!q((N2Qp{C;%@FY9=Wf(Pc$#y3}0yx-H#{yPG}-+eSw1yF|8(h{ca{W=4*N{Bby}yO(WUGb42e-1b%} z_uZ+t-=4bYpne(LsMSD_n9ci}mp=%NYm_?RR23D%VSTKVQ?m75CX}yM_Ib!|hIc%Q z{nWB_$9BPDnige6f6m#tHg#iN3gJoztixl+anv~Ek908yC+^s2?X1<};U3`Bha>ln zE?$VnqJ%_au?vg6_e3MDz%lb%ZF_o~B9S{fr#fy&`(>-aIF;xF*+oWE?QiY0?tClN zK8k)P$2+1EYON0Y$aon)jeX4K8!%qR>YOA%lWA5@3Hk#ZY-IMAoD9*CtfN*3$(ZFy z)b}!fq6k%1#t++-ls>&pDV1?31TIN=$C4RkJxFcqxR-1ZNhE3iz4!Xsfz!uGU}?(S zbi@s#a3^W6kDVX3_iGplOn}a}(AYT7v?ZfoxA&;jCh{7Xml6q8$RWdsZ^MYQens}2 ze0X>`JfgLJMV)SWex3}G@)RNzCVcHrou8jS?`}qn95JxnbvBx zODuiOd2G#6?vJNld~xcciuz?}LaR0stI52k8W=(S9uGN;C%;BNl+~~xAK}knl?D7y zz#aE50f{N|KvqINM8I+xi9+%i94WXVvwiY%!2_1%F|mX-955SNxNHW8)@rQW!*i5K zEePuLp(>pm!5t#g3qm?wuu>x-^Tg`WZBDv66*6;aiC&`Y@g+9vJXScP-j1BG%hU>q z{bYg}R{WGhtWZhqClKqp`1ZiZA||^^CT%bqYb7d;-4yxQ0LI%3Khh~gOPKti507`9 zWy^#@t~}ll_tXNZmI{+LSG^FX$6@q(ZS=5XpQqeD-L{0gOR2Dzm71kcbeneT?0UkD zW!%Ai7OmN#H%$hpZ)e>8NimSx{M`R0o~Oo4XlG1l#6n z@=IAt1T}!{m?FWoM+Ksrzh0VAPtY$fzWp|NX6Uv6@Dlui+lJ^blVDMkq9aq_HPI8W>h*qf@^alo&O1N_w$@W|HkN$jKKh}$t7axu- z;n1#{y*X*eWF3GJv7rtq%QpEaT<-=!~Q+KGm|OrZhvgjq_Zhxk~)*V(=zf1 z0w`X)>Ke3xF^Qqb?lRd#NsUR=U&%aE{N(Bub!tC z3X8}GQ??PCdr&D;IShtPp4JmSZvqC^o;usE1o;u<><1$MPEX^UQLPWVoQLY218B~r zl&S`OmSKyCp(;>6`T#TjMVJV6K-NZ#1g?4?-yjHy1e_ZlJ3>?YGW1nX{Np11?Vkv$ ze#97{j8Bah zV(ZuLDJs5={E`*}YUpv!IL3(A?DuYJo5z!RfcbPr%YGT-{BJ(}YX=Y0cTIhl{$OF@ zmrI8iPfma7s917{4AV@>&lzd~QKN^}1hLuqJ+U5W32+fxE!Ly`agts>PKINBS3|ZDYI%7> z0sUuku6Bz?ttCu2ifGlEEw%JCIX85@)9E4Epq1O{x_*fBeCCH06%@`dm3I;&13yH~ zJ^Z`}vkk3q9(_#CIQru;3{HrQcQ$ik{n&IS&Z$K-pC5?T^A_{tk$$;Cv4jkA>!D5o zu`XXAmZ~mz&FqBw{13Q=+#mj+d51=$-asqJPd6yl>Z#Tr{9yD3m(#;x_JYE|ZWyQj zNZ(LcNER`!0~i+&eql$#ExZM*69k_KU2(S$QI4){R3>ZzsiN6=7$3TZVBeEAkluNR09LGE_KZ94ogImw9~Rt{PA7C8T`!*M=?cII^DqmcaK`-;zLYGWwL>|e^{sU zR}2?vMAdPl##K>Xp;S~w4bFqjU5D*Pe}y1nu}miGc3G{pLZNU$TtRrBa z)F+dlJitI?z>=>|)O_;5egJOPlOOjpK=8bbgNNSZ#|QeaLkb6vdXEP`{&6_HX5UVw zhghhLm6vam_J(lej;^jdP&Bl>t05A;v!@5I8=V{Xz099OtlrmcnE+(=lx_}0R^gAj-ai2we_d%4Gi==x9$V|7rez_xSYYyU* z$FK&-#on^=u{a;AxlRR)IMb1T-9qXMCHxALn6E75i`)`@QmXa}-nUxlA06TB5j*(h zl_WoZOX&tuEtK|zGPfQ-&aq&PTj@^jlNdSE!;7J;^#)G%B(i7kZ%3d6t29S+4D;0q z7}m#97_|@85;LtWlWMJs5S3Ol+1$1h9AXhQl}Q&`z6dHRt19DmCve0n_VDcjZ<|bN zC?Vyux0RI~ntfG|N%39EauZTB$n>#qrBmJAsr0vw(dUkRE0yZ$Nu|C;GW3D5N8*W= zmPGuKF>>?7M-#!i=6K@azii5AnJ9qU7fRfcp`NAeR!qQs~5{F1&slJ zQ&m+ta3~x7zJ{tQG#xB=R$}7F5+LRK&QzBzVB!T*ex*}YW-6nc+WlW>OAQR9+P<(~ zd*BPHHoQ!|eW$MTzGQ80du`o)J-VI;lQo@fiTD%Ck6oVgRfi5!CopGqTW-!=)H@#F zcIQVi`vT|yeawEN(Ez^5{})HeRY0!i9<0{2#@wJDSO6Nhg+R~l3d1YnTV%ZCpVzGjOfTdL69Wa6B980O-{*a8(PyFQ$vWb3#lmFjx7gGA> zhk41HAL2Kt$v$yu1n4~PXV)PYfz7SM+Mw%)sDM4wEUQ@?F{U0{T0)@nGAPk1%c)-^ zE(Vro_(F8P9#&QWa8n0H7U}Z zydK!n#|nIqqo?xjzz(wIasb-!|fV@Br-tjK{}*-4I$*Btm<%p4yS z%E802c~!6dQ-ws7-A5^{U1C(h{9S)#q>DXV4o+NolE3YTlia zn;R60fJ`3KdAp=y2^k?2`YAiK5~oOL7aN9xO1i@>G66IK9V3X0_uLeUqD9%p-I;~( zyv^iPNPuamh)O9=CL@b0bV-p&W;eL!!qgc}liFA>mMW3$=CaCL&GxO{%tF^aVSl|= zC8;$SYQ-vDeI#^eCw0cN*=9*8tgfFCab=~ok}pCbWw%pRDMRB9si3S}S5d9|XIH6J zA`Z$Xx0;$jXCv6P)4ZSM4FVwney+NlxMm|ZtFrhCsi17B;L3ijEjA4O^=279yfLCR z)nz&DO0;gLx;#xH8IER^RFtsmkUT2iP{8a{egSr-Dx`Ixf)C9RWz|jI?ggXC2_~Z8xl^NWGKgI=MWb0MGu0`?PLVO~@b}21 z(nsB53%rTU{wTQVNy@54)*=0PWT!=I$bvS@qHY`Ou)poscGDya2XH7aQ zHAxbIz%BjsFU z9a^1Q(e3(^tIQ@52c(l*%uUcp7ocBUm;V8HVKcKMKA2=0z+JQXn15$a{+?Hj^!>+f z0vLihs9NImkio6g1o4BgR5$zC^mytuHv|P{` zb=4z5g3MFs(z3TLG^`eDN=o!%^-!>VKh$~Nh2G=Gd+8f9?huNN9Q8q6znS`0#%=63 zgiwQIp7JqXxb_?Uk+?xCPU!SCVzs^@5Imh?d_hmrw{yqg1+!une3l1YsjenR*DFy` z>JRB%cQKii4#zFqdJkJHj)DcMHX8lt%+Z&V@kBTr`NSdmcJc<<-PiNQEl+IuTyOVe z#N)l8YlKe&10Zx1~-|I3kyYl@hjt%zr16dq_`aJ>HaqX6?;(Zu=GzobTi1!6W^ zt=Ep!%$SUJ>HtXugLe+MUFG$6A;L}w=7abhGn)2f;{IN1_XEjfyVVGTt^a19KWfzK zw?&g@+pIfY$_8N{!+5~+xhHr@28+B-zzZ2bu!aI&_S=WwsrDZdb%g%zA5MIQ{(gvP zzej%;b^1TP{uQDNkmt$ir#3xJo}kY^z4d8&>FLc+(+5b`Q=6H2zQ}!pqv!osHWrN_ zj7U@9BWAL^m%ux@j~9D&_g9#!KH!NsU(Oe1v&a}5F1St-tT;7R%@R^BSNX@x`UbVO zL9Y#0RSL-%!ak~zkhM(*CSmO3%?(Hd6(&AkR4ot$G?gk}k6fh|fv@r_C3qrG8=O=< zI!ZGcC87`z^T^>_B1v*1?|aWW51nPM_z|x7NbU-z!*&5BzU54U=Y00$S-6Nc&|jiI zm%oGe99GPP_<0w4s4W7i?jd|00wIhUy4J5quJE4V3h!~Q@ZOc~z0!MEy7x-YAfMUK zv7s**wU4M}@(!o7T_#si9S1lzuiruKN89X`H(%P%u`}ZHcrWu==FJa%V&+wR z;)foi{=|pwPRr%0$3OIB<~{BQmUzr+Ym$i3i(b-1zp?Ju%n#Q8`t^0c(l2FyZ5A{4 zikrE=Tld$@53I2@pPc#nx?i!cU#7R^7xCVPK3AAmmh~|z;JHWqrb)+uJt3`#gO<7h z1HObjB0{@AV&aJmoDio#KnesQ4)wJgnl~T*`f2>2J^|P0CofLHJ=($f*cM`Jq+ieU zZlS+}RR1$u->$op^X`_<*WVRKl5`*U0@+MIOLJ~)*?Qz#%nzz*+ZP+}U6yV8V$;2x z;jP5hM8BMA*hatKLKxZq&p5HO;K!U?_9c2o8`g#rOiajUT`-}5OXxQTNFwtD>D%;? znq*sRP4c6g=pg!#wH|dk{D{LJX(4|i9|_Q-cakVmm9yi)a41H>q4qj;hPZ6DL+R84 zcpt%2Kaczopd;c{R@ZI9^95@wE(@~Veb#6bf4kM{4I$y8G%h#QS#zVxgq=7U~UL*Gly3XlFvd$?9A6~+vA z0o+Na@d-qdp|;Mi9uxLFQd8S0t`;}hsS0NrDM&#_EmMQ}7S^w>pfbyuV5=~z#jRUf z{tMt=$d~8`vj{LG?{!?I=iRaHD*fS%5XoocALx%TNjLbc_vzoFO5{Q|Yd)Kw#=4<$ zUZb39#3pm;_}rT1yO-XX`A9TbXR=tY9VW9+MWgYqRQlVe-^u3aQ~#U@#V^c`JQR<& z!WG)z+WNVJC*Di7b-|=(xaC*q54iR41H&oIM3Gh;(~OkB7yC3Q*!V)km8)C+=vD9b zJdmt!q|l`cu|m!0oqm6WkkNF<7xuoJO`@X;h}q`2exz~MYLD?J_=B<7O>K+MM8YAG z<>EiZD!hsLRpE)bynNQ3&paHC3GV5e=?8lzPcJN-o@^uc-%ZyeBYG#1(*Hu;1LFB4 z{ZpdIbWs1!jQ%}*)-*<+`z$6f&wduzeDojQ@CP=b z>o>naf|r~59HfP1^Q6l#CJX`Y3T&c~Sq|zLIeQD;LvB1x-*p`21_N_hzwpnfuQI-N zrtIe#+&_Z*AHg`TaR0~2*m-(}%-l(T?QXh`v@T}T%G^{DjkL0z z$-ZD7jYt{)ow+&k{v5d}bBR;MCcOJ3 z`NH1NNLC_1`}$3GuJHTU-a~li0`DLK>bsen=g6741e8Lxy`;D( zzpzLvudcCB;ZdD4Sy5KZudGVg><3#ojnr-yl85>yVo+;0g}pO=*Q7gu%mqE!MwxV$ zxz_NZSo4`=?H;qmNtltH1TF5zd~{i#dX^ZowrnmdfF(xOM-6_UIXOq#=0lB$E@+5Q zEbfY_+xO~?T4cJqJ-1DSGGAwM*r@!l%XO?RJmYjWRZdinMQd)Z+451;WD)X3Mibq0 z>0g*mZh1_H@Vq95=VYzK&{{8(khN)0Wq(=a&KW(p*z_Sg6^qCy9o5xj9fnG-h%T2mg z%zv_{{(vWO6TKPk@5W#Nc?6Z!B0tZrRS{I8Pq6LTIyjE zP23ufcD9C+chu3Zw%lAB+^pAc#+%H_9e}-f6g<0wK?7!nGLYyN4cKwcqcDQt*Dv2DuT0J`LP#`dZ>P$c@n)HfpkGjb;e9Gx` z5t&TZA4yJ{wCECCNt6nCuRnOaz3u8y!(DXKhQdOXNZ{3I{Z(k>gMv!<9g$t&>4FAR zGE-5m>2^e~Ynt}Xcmo?XTBBa6?RI*02WGl&j(hvnrDg4PwI3VrtUKoSH&9qYqju2k z-xqCfzpbWb+Gw)t)p7+D-fQeKB5*GhE5x!6tFc43D>mz~rxl70w|kDE`C3$HFrCrh zM9-eaT!9A8KJ-xP=er6Q=Yhp7%M37O87R@n7MXB=lEuTSN5|lmuvN z+)k`8RP`)AU{0JODwm}thDuQixM884sm0N2wTuhM=k=Z2F?M&`0`*Ch;@Irh4$^zb z9l25*>MqQ~3hKX(_ZBk3kQmg*wufb@m^u6`PsmxW0Zoo)t$3#jm;{IH!%e)^b~@~^ zz&u1k8&RuN8myL zMM?;80{wdZcsMwq*15||N(eXDG-tC%i=JC}x4U~&Iy@@2^7(?EhK6S*p88b!OuTWo zQEvjO!~Xil^S$yyqf9;ETh^t{#C$_)jX@<>4*8wafrFT@7M_T`IPG>uRx;Qyn4*b| zW30?Cciu5EHmgy@u7@D!k7V^p>bF;22!(^UZR@?pZg*1DKVz|d{(E>l9YV6|!Edb^)kj!I+UbA_3)lg%5yqUDdW9Pe-0!>rsM~$GR?u?X>tVVm{40`Lj${9gjBIBW4#?K5F8N_$mt$SjDe~i+ zY1c;=7fJc#eHb1fUITjFXqfDI3{;tn$3HpGQBt>O_EWb&Bl!$k^(LO03h#3J`c(=o zDt`x4t#8dPeL0mH1@S>kfm~DLoNCvOfZcD90`ryA>asLJ?Iq9MLvOriY3b{|w=q>q z#HiO#HE4!TyImeqAB{dp4UnJHTJi%V9sJH{u^bz%L1Z&gF;O{y21(6R_lH7BJoO)t zvsjLKEAUh%T_RV-%bc--m8bx`DtrwI&d*cZh{aOs(p758%CfyNHNZBV&peQ=mwGT$ zSUXcKf-_(PkL`_lbz^>EW`LR7m+_Q5%x(24teji_6dZ94sass6THG#v#3(dYlqrIW z_5((PX({tkHtXZ}nHO4T0R2KpK|Y=la5=9@bMIg@7F1+__IeQk^5-D|Cs$p=uGs3K zK?{(-&Och|r3!>LmQu6%M>%GbPc=0jbUV{Zqb_B&ZVol>v$-2__?Az?;X0lS-PGT8 zDwLc+>uw^KE63u|Tk0BTZH@-uY;w2+JgUKIMIk7ToJ%?)wxi_3pw{h(CeF0%xix)L zvg;bF$>w#qZ|ZBlxBadDhg#Z}Y-Yz_Zq{q`+vD!tp`N-E@rDCtgB5G|QSg!)>=fg_ zAy)!pTbm`kng(eCYV*T%`=R0M9acMo_1fNcmEGZ^mPmU|@`&bp8fmp^B5Ys3Q}ML*0=&W&ZynU50h$cc(v_6YeVIovX9G*}3wmhBTMC{*!R1B6YQ-#$=9H$@=I{7}r&XCjt9OLQ^rob>pS zYHh5ptO_cnei8SZnU}@33Z`qfb#FX+=zY4l$|O)lt0fAeMP8BL(xYp)ZgzRbbvi3r z{f2RRBA0%ZMIYz`rLi(FyNFoF>{7C)Ij7IAtYa>cw>Ha$DVJvLA2y)7lDtD%DD>Ez zY=vl7Im=5+38jiNj&pgGvGEE!U7M18)S`+vpzd3?JLs)EF`@^)sv>5?b-H<<%^Iz!2tDU@r&UwhCCtOZhX?;m?iCWmM@l{p| zh}O%`rx1!4x4Gb>m8+7$&>e&H*C-OGnK9}uC?nXJ{1JJN5{o5mzRgu$snAmHGo#PYPTEYM#i25 z;0<_Kg@kaq`GN{z+$vHRmz43i6qWfkg+`af#YGCCFsPFo!ozA{RaObX*(hGy#a~-{ z4Tfa32tm)fsB@v-x832{=J8K^$@VYp(DmCq;|5Pu2qbWc#8bsRlIhkDyV_1gP+XQ|o8I%#WekRT03ZK;(8n49JD?pl@UrKKTs?L&I3S-3IQq? z(26w3)8z6fc;x)f-9h(eQ@EC(9@3!JY>c{h2JF3hvOTlQ+GjA9khJXVmC2M?39nM(6JoBnVTZ574kM2lS^SJ(XY}6x*;Z4TIYJ+W-|aex zp~4ciSX^(d-KLl7iZ^V4C@QEf+b;m_KaVFUuZmiQ%Hq;;E|-FjvY+}mtbqsUr>ui| zYaPO%*uI#3HWAqEc5nCkr#;N}X$NiYF@u-czMxVTK-M)X{jfuf-;1ddTjvSM(O7mH zu0)h8Q$)&WQBjFa1!qxws~&j@keZZSe4tW>WQKfUc~!&&gs@URm&c*z34I?5QhxP} zsmow*5er2jpMKo$-r;2ycH;HT`c9*{O<{2XY2GD~2E|l{t|q^Nsqrg}^kIy2H{uy_ z)&^v0Dtv5G4x~cuTVEq=gOrcUJMxx;xg~@2-$*IxLjEsJ2to^9N=^JWoduc1# zP;*r{F{RfS!5}x84a91&U7K#Wvy=Wy_qoPq;0VLn3H)%3z6CaH5S|BC-^(d{>^N67 znDyoPYZW)@Sq#$SbD%QLr3ApWqv_OJGgii09Wk3NM+YJY!l8qa=zv_2_0Vq2=?|VO z@~Pahyv&SNZ5zg8EF}NMaF%k9PTk{czsjj`m6w+ijzNA9S+}IHs8A_X_C273D0F&f2=C#;0CrXzH}+_1hw`9Yz3doZWFQ5DEc;=b;$@ zWm=sUt7(6_|A1brB-0wLb|&36YcLuuCev)w#9e-W$OE6#WMaw;K!=Ke!EuPOYF?)A z4wHc0xg1pV-dyHj7B-Ac$CGVBIqHOd6t}tM>BYG0h zkPx4y$XrhnR?bN(n0dbD%{Pb1Vhzn?K#*SQM$YGXFK4YPhTCRYR!-@| zuh~)M97E(|L;H(#5?Qm|*(?!D_Ys3mzoXvKsZu!jyl0ocPwDgt5pW_=S`eoBWI27n zY(%@Os*oCceHG4b5Iz6~J1tv7q`3?O3LcYtgb;!``;8T0C7EeeiU&A@^f-AO-?2d8 zp3TwOKw)%$eCf@mrf%{&{*ms+=C=;T4u%Q?dn23A1p;wG`G=nvsoMHXG?Ell%#`ul zTU*{+5{x`CEI;yIOKWFE`3#?56OTNzLyib<6GnIr)ac8`5}***u@+vA4%bs2s~)q- z=jVV$oKzEJGh*9mS6bPwJI;CnT5Fb;i zH5#>gJnkPoz7bx zs!~+AH9BW0>_?qOyG1xdMO>~UO7&W2~*$AJaErfhKC zl>YY467X~C)maYx=-z!Cvhgd}+Io+t4hgxHf~tCtrB+xa?#M4HG^o^uA_U|3#cGM# zP*{{t^66Jm2>BRyBeJ%4T)dC_nMW++CUd}VHf^*#Vwo_T{AQhY`?v3vHmDR9i$c{P zy;n7?suEk2iUzthoyLyb3$NRqkd_)IyJ-i`5yRQR$=8r>0XAc0Rs=j1;Y0wriKu~> zwsxM0aPrdi^`G9=bRrtvs?(WzTAE*<-To5&(~j#ynV&^&oS2Tow;A=3R@dIorBj1U zPEoSvY}>xCbaad-Id3O@^iPN|)R2d~n#865^ivjthcmt<|+Qb#CpE{9Er694@k_g#O+)IS0)e&9K`uq|unFtIBO6nYXf9YB%UN zdA*z9w~&kB2^3i>szfP;NE}sb65?v99GUbgRa_)eR!OR=0&+zV{jtOXp;Pvbqj-|hpXa;3BoDd)lv z{ZDE)DqZ*$6)u(9jna*ZigGtGDC|r}vWhaB0ZORM39I8;eNt2{p*B-a zgKom>o6zfQ;%Y%aC393(iH%zAsLwmB)mW$r3QHc=Xu?84wL&C{sX24#pTX=Us95MU}Wp=$9+8mm(2%6?#0t zbu(iur(8;vmX~?-N;f)O2r4U_%0MSvE$9H-?lne*0x9qf0q?5vU{4wBiZ++b#%vzH z+#!cMr>u-*?)=}Of}<-`5V`ah?z6n-;oaSsH=lPCQ(@UBVgxs{>8#g+aaQ~_5il$` z1SRwTVw>Ajl7LDclsJ(qTNO3c^c7CJ#tWmKNb+#Fe%7cn*GICb zY_|l1HH=mC{$sJ#gQVnA2casy#nG8W_h=5<@aR+f<5mt`_Z97{dUO*Lpi+hTfG1iDo2Xte2|; zReX67->1`WjH3Ke=Y$&>#T!nIsxJ^7*Xxi0$wb$qfnJT?QCd_uj0yyZp_oZuD@Cb6 zIUkYv@^Y(8TB|q5QNxDn-sLE71CULtV`f|!7 zy^cW*P#;8+7BleDLI+*0II-zW(-Fsr!C~F(4s6rwtR&%b9SkvlOb{#JBErtGeupw` zviG7dj#Z=XwU|?4y*Ont_iEG@y;6}jnY%k3%61c~oRM~AQEPh57F;4}Gh2Gq8mmsB z=(d?U7^?XXbUys2d^KayH+e2=)R#@G%cVQ3(PQckB*nISm3~o$u0adPSd$qARMmC% zs%TZ2BA`so8cpUAp58yKR_XGYMvK`n{u6(7y=CUGNp%%3h?ZE3D%iDy!>M7Lr&gLD2{&Ov~_0yopTcHWT@0M+~ zh@+oPw}!@^?Y@Yq;ty#4#Tb~3TL?_T0E3&o*q8mCpnxTy>)hXUJlrb6w*cR3Uz6VhPN$_g5@~+u@KgweKyrD6zG%_uEEc`a zOqF?~4tLzAl8Thew>`Bx$vO5^V$bq5$=y$Jj_pZ2MSr5XSs{~MfA78DyZ2tY?cBNV zuD-hD)2P)NTA5`2ac%#vxc82W>iGV~@67BjDxhGaC{4N`EU+|1r6Y=9!-55uPNYc@ z5RoE?8cordYSi>Zjj2Xs5{-#5O^xY2n#3f2(v9iL@_SEV0YAz2`~3cRUXL4c@0@$? zIcLtCnK@@>?#yKVkNQrDN=%I6|ND-F1_X^7H+Yc0Q&3R-6u+P#KmNaAXUF*GZ7Ck2xm3$Q$483?DvWv@K#s!)wcQ2AyHfm9Q&e z?2XID@DU>deC++5og!RK@y2LuSK#UH@8eq>9~eK@E!xAuZJ2|9?VdeX_UUv(HtKYI z%w)3eJUeMPhTb?Bw&RPU#r7Bok*wW<3Raz@^ijO|k^KuhY5Ael@9J3=1lc0 zGA3s9=rM5t2oxwKinlc77uZZb5$V)#lEF!1d^x-!65rTaFQ?;+78 z6o)Tke{6mXGiJGr0sf_NlS*&~vVPC$(*|!oqlVALP2Lfi*h>=sW#Wu#cijPc$}k@B zJ6Z&>DFzCax;Km+GsAa^KB{eP)Y9OT!q>>Rh5#k8F}wLaOXXn ztM+NP`HUQxwPR9L!gz1*rKaGSZezVNr^MYp-O|-aQcT4G0pS>gDUGxoC)$+aprDD7 z;{%JsNnlVw{k+&^-rga$L&v+j&yLK&mYeR~x1;SIGcRs(wktMDrC#JtY?Ux~%A_nO z=MiFFu(juBosaHLzNTTo$d>ASWfdLpp~`@b#dJRPwx)ofh8c@Hu%8$q!NI}n7pK*X zA0K5~ZGLR^GcEl@tz}2U_qkZjF&XS*V(-lTnq;P?zF41EcRVdMla`RFv9UXr73A)WiJdBR z@g{hU0WVwV0;{wU5{WfQ(}zfZGX82SU>r>n!BU&{@BR<^NRMfjI6knYhge96#TG=@z3A zn*mP<3Er?csVXQS+J2S&tVwaVW>;gQ0ra-wguEKuzqokovfC75mT&5N0IO+bK=G#OH&qqk*eT^AJ@Pl;~JmG`C;V6NKD z(<3i==>{xKjll8T3mIQ~^(o4w74&&6mMj1H+8k`U+C#qI-u*7c)>o76!^*XY?o)I< zwXr1KNApmZw{zLP#k|vIW>UnOx}zw$e9#$o{C-bswmzW^@KJ6wXfLr88HIXiv56 zu$LIv3*RRI)+M}Y)dBSnZ3ccb(>HuRdbBQv#7&&^a832YlP1Oy>`^*te{I#?iIb<) z5$vG1Zpq?xfkD`q5E~A)FJ8P3Tk28!>eAyGGjbNs$T(h7U2<$j#?l4TGESVU6<-I= zoHAv{tlF#{_)suo@{}Eru(~G#=S|*;nu)IJ-z3yW%hfOcgPhqUWorD>b#(`)#wQb! z5tNq79ooR50`BTHR5A-+;3i%yTur4gt(P2O`kr0LHdlBDyl2^ z?T{gFPCqoRVDjWGM{5_Lv6(h)a@@}Owb}eO4W`fa`Yg(CB1%N=NDtpmZknN*Y5sje zDnbhprInHkAJ9xL(sUfd}TsD^ji%5B2q=0IEpJ&SA6*j zd1WM_!aCGsi1GE!`G@Axu-mwk6>bL`>EHJ~zQ8J8*u_>Td9IO=Ew@Amn(d^XrxMEs9d4Qyt1m6O}uiVrjDWOCt58i%?Bmkmw+p8g!mUoG|%$z zb!3NS!F42$-@Pj0`S?jxUfAM1Wxd_t5-r{e8}c)Jd7BOb@-M2Z7rX}4Tc;`vH!`mgaFNFllNb&g$+O7=x3b}HCKHfc%H5RjX zXBS@y%`Vm`S)%)bZ8iLc)`hQ6_*bvXH)6orHAGH4XL}zC=noqIe zUeUzZB0t{{%X9OI+hk{FoD(0gJk(|^woeEk*!6qjSpB135pd8P9UM*L!e_!aftNNj zJeoNS8yY%pe5}JzyD5CD1`k&kw?TurFG@1uBi;-j(M8%y|6a3yZ{ZJDlimEU?%~^; zv-ap8{RjW>@@X{8uqnEqJ-|l6ixXDM9_Zdd_hVL^k&C(&Y`b9-o{SNDbjtJQud1!G zVRjO6ri>%m;9a>VElXTRkBas1h#57?#bqS!JYzf+Jn;87)Vg zqdh%iu~ym{$GCa=Mn?L2N1)-S#G-3w&H9(oAB}=+P7x#gF-4*xfM^+=V0*auUD)99 z;vy{O#wJk1!^Q;KV*$%{M-n{h)z!6QBb=Saj~MMRVT>!zQ6d|3Z?Xf1{i=+o=n>)UV)9B}#1eI3 zfu2TZXOmCLLjC>Y_^F?m#f%&mfzNU9Z%h^$?-&^4;^5%AG-+6%$#s~6%TkmE^|#zf zcVP`HFAem;bXSArPV%k=)sv7?jA;zgnz=R#)nV>jO6&MVBpZa&)4y#72ZPrJ*N4*dqLOwR*UphyD*=#*$oOoPF!W*woaC6K|Pq zSyWGq6S0|8ctl`eSuA-2Uq_-FUBulfwEi@T{6suFumU0>2OlU2@A55hdrXbZNuNZ1 z)krBFyRG{m_1tW+ISikKZ&0T`M8v`){vG0Xn6ryRzk&U{3;VJuCS|b0U_ZxzpB*fM zZRNIO*|``dgGC~+gOfWtpur2yDd39NUIt!-vy3 z8*D)%kYj7*7A(yIe4}NuE=PYHaJc56QljNs{s8=lC7yq=40wow&$s*y_%chHu2%mj z;IKM@pV>nI-vjtCeHP$YiX`A42>3F~%zpR=QWVU&=40n4^o#~SY6do--jc@50W^xV zgIvZQ3uK3*4eU9bHoeC3Y)|FC!+ZPPUh4FkOCN!!h-eKA&@90-=ej^Elv<&S7x`PE zqpi>-K7F`$R_M|uQ!lRmM=R9)Qy=aOD>Uz7W-qScp+3HYQyO= zpdtnn^3LriuY_`WB~;2QpIYeSZ5PiR~}!h09_K!b*|&UuW>CZRkZ61upF z>s>;5J|uKWS|2W_%%Pm}&OXW_e-hj5$gzb#CHUn@eh>FSIX?+qT+jJQT+UBImn2zn z1zia>+jFS!L=2n)zDq_v7B970ivB%7rByBa(GR%N)`nQ2xfiFSl})qc>1yGZc#Cyq zaKf-(rs%}`CiBtJ_>WWHX+%d5?s(A~#aIiCsB@2V@$?^q-o`It1_tJ%J%cBV8|#|j zIjWm{pP5H+5?FN4y4#(GHQD z*)litcy1(=>qA19c<`J{sEZX^@Ix4&C~w_t1BdcjX&?o!bEvdAt`BK*QXc|p;QElz z+&`yVanD+z%l{5U%lIa=#K#^2(Bt)vJ^Z7V)Tf15$>rEPFhNtsm`1z%phu4Pg^rv? z#droyAl_qLKFqr=CHOZOX~GXE6!F`*;HI7Y8j`gg5TEw|-u0!BAAm& z-*B?L=S=r-z-1f7>#%I27GrGyr+?ZqlWWfkFTr>7Tpo$fZJgu7#>aC@5UrG`^RnGp z=D_Q`Y`eH6NNDZ|ZV4PZ$>61+HY6XjGF$^9_7*wK6_gi;aygzwJ`s@vn?qqYJ>4Ro z5}V5*u?t@5mBS=xESEG2IStOyBy=vJOTsw6?f8BS5jQAEXi*3ZQsHvC5?av1>58~j z*uU(Z3$AdeY%6&y%v(inrMHV#SiprgWNW$1j%!0=b8Sdyt^?PGgz{cdLi2|4UXeqi zg^ncUe61B;3L<06%_1`9V=4nS)%Wio8UKTw!x8MAX^H%|sM*fR&e6lpVfa;T1z$+x zkyrmO(KMxw-XmS3+ybX!;K^mgJiAdCcJ%j*b{P{sYQmw_$cu=Zh4u@eB?DP9g==5- z9lY(3eaDgjD^&C|5^BE0p(1(}r3$YQdR~mvWO%s>xSqF`91g!Ot`8eNqkUoldVO1LD0X|s4pVcVfKLCHGz#pRE zdpW-52;VcuFbDV!2+TR1H-x9QANUUlxU0f16MQlHs4M4i(xLEQ6Yw*j<8P=i_>nCl zPmRyN)hj)GOaqr5=@;?#T#-f;g!HRO`--$dq}!185owH~;`wf*ai|^M#|Qw@$B?!Y zX)sss)V^m95B+{-T4ISv(MA}89g*-OHQ96(Z+B5h3L}vEBXM-Asc@qi0NYViB!kUf^ zh9GFEQ)sE-mk$BG15#xh#sj)hLLEK;^svRO8!d3JmC!*PSL)fxs^^A_F9CW46ugD@ zw+r0H{apL+LE1vyMC1k@4bmOJN=GDM;OiujK5bdfdiZm)LZtD3zE&R3!$i6r={%8+ z5NW|%|E!g_`3DhSaNYtcd7D4xP{E5!&w0Vd8z`4v%E_gdxD98y^x_111Bc!!aqT%& z*oMteP=IZ4-D-nLI;R1t0&V<&jW1;ZU#u088E9iRLJq_~Jg@&S5-CUC`VT{5QAPie z<$oQC7GuBHFQ2tK zG$h$|q~8d~P%qB~ladR@d3oVH<+`-xI7@kQSg3K!;>`L9Aqg%c1Dr;hd_1zDhpX$t zzu96H^r8uywH2kPVs^|Ht`upm=beU&&@<|g(C0!j8uZ0^wKgHp^MeAOtND=%z(tEB z=>2n@7UjdA^YQ_{C`W-W>-awK^Rfv5-TNioolcRStp)fUAJK;y0zkc*g5!LUAOl@Z-Jt=j1|vsMmk!gJw^ICyzij@0QqXL8*MS!DyTdKs+U9?1FHvq zK|Oj`@*}=ZIfAhi1K$}4?~DF}w=Mh&6Hyn+$qc~ZtLjz>Zt%+S_Tn`|04>5_oIBEwt=JY%! zdJtJl;BBf}lGC$I^dM6I-vHlJP|tagj|oXzeA8-+Id}XHDC(asS6KKuk-PPXWpHF7 zYUV8>4bAB;*o?wkUkKjydRM~z6GSw+#rhur|5CzrUm^!Rni(TW*hm8K`#5h}@z=e8 z-eF1Qr+5N-NJ1SS0<;;tuv-=6MD63aghTHF6xI)@yM&J5&?UG>jZNklu(C6P4+ z8XqDg5iP48Hm*Dha}T%tM(6^kIlxxV30QGSESG`uJ9^xp#x zJg&0~-15bl*P?HFRATr1M)Co_0bHUuZ@v|qz~;O~4{5;oo9ZE#Y2Jj#MzU?Nm;EU(580oZH6Q6K@)lSBiWS>D6zzq=PO)O=Yq#{n22VkoN1<&R$A1}?XU{!R zvPVh&nZUmba7W>ty&xqwaveVeXr#bKFUqyjb5UTUKIwu5_6do-l4F-a!k0xm=fbgR zB{?c|+YUIs*M==wwz;{AhdV>c0^bqMV;9-9r$cFY+;KBO?-9*6G^7qLz3cgCf zi!3MmwW~IyP(Mz<_W<74vI4F!=M`rTYka zm38_io8ETYhGw9YXDa+#4V4PtKr{a0rHncS>JRi%r!!#dDS*#ME&~lkR_qy!xBf-I zzTM_`EB18FUSM-sb{n3uVox8@i`{1PhZQ@$=SLEyBndR|9EsM2reEUixzLLZ*Ei3n zKUtzBl9|ZY0R8asVDXk8S(0YYpAOK{Nh_~Xv?lYcysowKD%Fip`DnBG#fm*sql8Dr zQm2nuv1i)E_F@m3V#O}$xg>mbiQR3*F8Om%FSbpO6}!CWO)}d``-m00{5=Pb&8@-O zUi+i9M|=HWx$bSZ{RrCaez|_t@}7t=dt2N>-M1R=yKlCf7T%m}ak&mB@Hknu&|LEd zWiu}eTM{j_uq96ge^c0!Y|)KY{H5A9vXIk9c}MGi0$k$rkw7O!f2*)PK_9kvN!T98 zzd_g@$A|4*a;cB5hA%_u3w(eT%R?i9LNt zFSf6+J&B#(^S!V=nNzL-iJkr}w>_apVS5~#+ujsud;Rq(Y>&SM+nXY6PigfSB(oDW zRL04xbccjig-2*;MGFA`3#GP7_lL&EoQ|Q?c%0uGR~PE_qSQVGoO_Ud+=CQ0P$A%J zEH8_=zPHaP)VuXu0i6!Ziz4EceFiV(Q9XTq46h5aXI-Uxn^bdrXe!-M2RM&fxjZjx zV*uxU(v3FUn~~mIM$a$SK50FVyJeq5Grr-amI>@xHt5$B_6+o;Ie;e#>b%7eF)^L- zGjF*$?OO-gTCu0YC*aszmMw+@R_y7bofp01AoQh*HZL{qp-Rbl`yy$lpW`05(2EV9 z%j0tMQi~^fLT~-_qyJ02g;F!wiKkLayze||!(;dlgf$v2%`c4?<#d9ZA}&a5f2_Ar-WF2?*M}1l|%QAn_xu_)E2| z!h)nMKU(pZYU{MooWA9x?j-u4UiyXlV9{69!6NqQ_FC}^^+SY3>;%41Slnol*Frrs z20AAIA1eAdqlD`&3OMiMj5fR-l=!Q3pO6I{AMl}ua=<12D&12WZ%=`*pDrSc@0BhW zigE+K-c>|5=~9D44VY$NQ zx;&)WSn+wACGnSPO`^?`)IYLPU%G~m5bm_RqMxq6V#O}h4;OXogyp2(m5)OUS$IG5 zxzJ1o>S&SBq#XMy)Z2(UnqdhQk!7#vQmFr=hx=9;Xd8-zCMEtV-P1yo!iy4`hcVH)i9_@O3Wpo^a0DhVWiVpj+5jPr#$grsA zVoxoezh&92JI3YZ<00@)DyX-{+s9LuZMxfpcf#XNz{%gjQg&PJ{gKaGXI1Dti>qlLCGh;8GUu^>NwHSn=%#;19RrI|^xf@ka>w5zq+~_+qS$ zR#<))@DqT$349*!gTCd0fKLGYGl4H6Gw5=vfI9+yT;TJVgX>tv_Tw$nb=aE}x*sCp z+OD35p@(ts46%nN;25*w_}Z49M*xR(x-r7D^ah;rwKU>{baBct<{0^xE&p}4?7*du z-d?oWa=PL0;s5Ehbe34Ob!*X;Bc$)V^m{CS@O_VaPfVvh-QXRqf7E)e?LY9wk{d^_ z{;O5IsM+#m{h2f5uHpyd;t~_%;vOuvyhp}ot_cnf#YwQunW#J^MPGbTboo{?F*54b z<+-;+M#c~lW+@2{2&hUAc7=uOL?J?G>W_G#Wu?cyNLPqPejIZRAL1R@jC_Z$LB@vwYefBJO4)kb7`@%P^sU%p4IID*HZTL$L^ zv~Uh)v?OQ4<&fmQY`C4Axvl#<^5w0TX>DYJ{t=3 zaG&L{?l$WPCOlF;f{7L-m&W&ej_1$e`3CFAAUslwvv?XRp69v+yLus((rnThhvqWp8MeMyRr`y_C z_?7WRmV9z2FYz*z62{c}q!A?e4*B?x-TpM$cAe#V%hoMq>wBMDvNw|!(sHdOn?7Ac zZ|vSi%eyO!y3=TT_gab}1){Zs8fGy5h1!jek(D=C9<@AnJz4qwXO{XK$a0c*ou!`q zRYX3vOe2RZzC{)ra?)ZVfAQ8?%+v7E95GKrv~0Rxx3zD5rEcrx7sdKYf>rU~Xpg}& z=)!6(r275inv(y<;>_Jokh?fChdfK(A>UIsI*HDpD`-3YlZCQ}*f+Wbx@&ZI>YmVj zp!-8VOz*G1S>I#GGHf%PwHaZv$>yld4}(Gn%^Z|B=;)wJgA)heHu$F@PD9oYIX2{H zTZ3(o?R48_+na2EwKLdFw|mC!Bl`gR&GtKoP8hm%=)FTP3|lnp6$fXBl@9kgMmy#X z_ZVJ2!fnLn5igE(9T_w7iBa~WI!1jodi>}GqwgNQ-^s?Q$ms>Ab50iLCC-PP-*EoQ z`J%JMWvGj<%WRirE-PFbTsmF0yFBXhn#*|?i>ob822FCE;X2oKyXyz8-??^=Nf>j> zn7w0;j(LB~`LS%Q&)7L*i^g6%_Uzc--G;gaxuv`1xRts!x@~k1@)+Uawh zUhp~X^O3KMudi>I?_}R}-)vvAZ@F)y?>^t}{et|W{gSZ7d4pev-wl2{{2ugs((ie{ zlYSrgJNx_i8~x+_r}@wGU+!P#zrp_||K0xk{15pb_dnzRiT?%vUjvo|lmyfUd=qdf zpeN8a&?(S6Ff?#dU~1r;z{Di@js6LJID|;Jjg95ASfbeN>F;xyrA!beh((W_QB4c!EXhB8hj!6Qm|#h;0f6iUY+pHgtHUA4G9P_g~W%<2$>&Z4k-(%4>=xkCghWl z3n9OTYN2+a&Y_b+Q$uHC$GOtb+R&EJO`+REcN>{;sL{pPYP{BXi}7yb!^WqLFB#7m zKQ?|H78Dj8mKZiOY$0~+s|b5I>`>T?;V$7m;i2IZ!*M`a_~P)Q@Ri|f!n?w^hTj?f zVEDoC6X734I7j$K%#J9DxIW^;h@VZ&5s((}$)XBi$pLBkzfPH)?2< zTU2mVWK=;^Q`DxYr=yNVy&3g=baM2<=<4Xs=)0mX$3(~67|UWCV{e}5H1URsKTPtP zR6psyNf+Z9;vS0Yp1geWfyrM_@tV>#<&~)^QxC@vj^7slR)T**SHg*eKN1}h(-XTA zpG*o#N=sUkv_I+Pq)W-R$$`nslXoV+lMnK3gD zWO`>#%Ph~_k@Z=@{AcD{ z7NjnyUeLMV$bvT(oL%_(B9BG6i;gaOYtdIZV{@`|nsRpK9L+hK^Y`LWi}M!mSQ5MB zlO=yIbzVAe>6WF3mKm3AS#~;iaBf^~Meb+I{g+oQZ(V-F@)Krf^NGB~yl3-$^S2h* z7pyDztT3W*YvI>L(M79^zWdkTAH|NvZpCrMRmI0jmY2L(8dzF+&4?AFSNvRdQ@Os} zx!hDfv%<3?x1zJ6vvNV@!75S}S2e$?s_LGqLsjpu3|rZ@^5DudE5ELetDaX~R=uP8 zaP@1|UsqddvTF9!d{diKTVA`n_E7DowO8s!*G;HvuRBop!YaBdXw{lkn^rxt>g#$| zpI3iPeRciX`i=G5>vz}ht3OnKy#7r6C-vXd|59&h7}7Ah!K)#-A-W;4VP?a^hWv(# zhQ@}DhU*({Z@91F(S~OlPBgsP@M*(^hD!~W#vzTP8a*3>8lxH$8Z#OfH0Cuv*!V=_ zk;YdW-)TJC_-*5FtD{yYtj<`yV0HfLiq)%Ecdou+^_kTlul{EBrPV!6woOh=-c6xR zlbTYSW;ZQqDru@|YH8Zkw7qF})83}1nvOM{YWkq*%cdWj{$4Y0P41d&*3_+OTeEr1 zEo<&s^T?WK*1Wvt?KNlDe7ENJHI`;W^U&te&F;;9%^}TE%~P6Fn=_l|H!o{0YOZcx z+q|*)rsiGEdzue4KiB+H^Qq={n?G&7(0r-cvUc#=k!wBI2Cuz$?Y^~#);_=Xm9_7z zJ-7D4+Mn0{*`l@BwTx^T+v3v_++u2pYe{a&Xqnftq@|#xvZcPIrR9y5_gl`keB1I{ z3-&5#b!c^N^==Jmoz$Azn%SDuTG(3E+SJz>vFtz%SFST0Q z2Ddr3xwLt;O=yd0i*L(lThNx@R?)VqZEf48w(V_qwLR4KWZUy?ueZJ5_C?!|ZGW}v z+a21+w)?k-wnw)owP&{Hv=_FoY+uv9p?zEXuJ*m{2isq0Ki&Re`&aE3+y7dpU+1{a zZC&8H$aM+pX0BVfu3%l|x~6sO*KJ*Q=emd2J-hCeb#JdbyYAa{zju%h`wr)hfR5;n zq>ikP#T~^RH61M-*LK|8ad*eV9Zz>0?>N)(amRNZe|H)>hj+Snj_-`>Ozh0;%;_xZ zT-n*&xuNr>&Rv~*JD=)2(fMxY=bhho{<)s5AGUtXdjIti>!+@tv3|k&{PmUVo7Q)& z-@1P1`iIsZSbuE&sr4VMKfnIx^;fzEb&c%u>6+FxuWNZ%Syw~Xx~}WGZtuFk z>+!B5U9WY$*Y!o$k6nLnFl-pU!F_|@hJp?EZ1`fsm5rk}#%(ljT)DAzA~uqvimdmN*8Z)>Zx-P$p&3iqF2F7j?|4OXbE&`xR_wPjc- zyGYB1f0Tg{|2^6(+FgL$tR2AFYcF7C^+jNQih0UoSe5pYwhR8~5n!Fb+f(5MWnlDi zm-aE%gPFB?+FN+98UD!*axa5#wdrk`pj2=-DSCVZ--ca8QR?RV_!S@aLTXyi-&9`AVxS{(R0q`eC0D)I9Xnmz`7_5VrD ze}de7g8dloR=+hU2l;Ch*9xry*OTJ65oN=F%R~w7M44Ru%SNg3Uxxa%q5OCW{`b#_ zGL^qp?Ru2az~4!vcZnZAq3zSFeou+=KLF^X`0LG62dqK<>a2Gi1p8o<@vsm1+oRnI zJNc*IRM-yx^-8`SRi-pMEq{Xy*J`EY zKiX5|)RjZsSyxte&$(Pj6GS)iVePnf z2BrNRbo!juiSN#y!8

3KwYi;oNp_tWVmec|z;S+9R+ZGv+$Ym}LyZyvK`}3EQA; z!nX?#VdX#@a-D(q4kLfLhyv~ay;E8uH2EBUEAjLrXnYf{Q?NGgIqa~}jB%mgQ0|_P z$QzI$q?Y)7?swb!*TaT?e}U}EKxRtjq6pcfCRUe=x?gD`$O4i^3n%J4PF z{2*kWfmMf1__CxH);|xc2QP>(+_)^ep~0nqVczUt{v9Si(RkuQ=72(qb`dh(CieS_ zMSj+TLkw~dj56DS_m^O1WioNl<|5R}pzfG`_zX4vG-~Gikn(A)Za#=|{T5#fpN1t= zW9931v?$Nxi^L$vworQ#tAaC061hk$WCQglrI7S3?K@EZ0eZa)Qtm=Lv=L>w4A#F4 z@9ycb;G2+qaO(eS1xNWi51#zT`St#u2S@%}sNQLXj8e+pw>lxMl(+YNF861Uoc~UX zpQQFI^1yQ;e>^XJzaVfxp2V*RtC}rXL+RMFAFHI!pxj=od30<4vYnj-oKYY@gdZiGDx%&H80Uhlhd9J zzP&vD?aKM~|4YMer2W&!kVF2*rRe?3)RL_C{iW-bl3#a1KmYdE+mi9?_0at(Xq&e( zD@8tdp7`GpP~dvvHSq<^x^!c-Z5YO{hGN!<>xIAb0Ho)+=33=CxC1RbBKtvk-S1+| z;nWqqWxXNI8teB~PNL~_r${jWhD(3D9<%1eLtfdNT9I-K_^2}eox>oXgXXyyx1Kzm zmX(!;R>HogTk;r`M}s^yvMUQ*brUgS!trn~ybE|E80V5>4I?$34aRtolUB_DiS5xg z#)49gwjAeYuf+(;R&p14l>AJ3=olJJ7tv?w%k)+HDZRu9V{9-RhHr&8vCV8NyNf-_ zo@TEy99YLNuc`~xrRcIS!cwWL*R9u`*1fO$T=$!v=pFPU+==^C_fGeV?w8#E@|fvq z>p9BP-E*9$pQq6?!gHc$oM*CUt!Inpy`B$v?(@>~-9x;v#i3V-*RpYRoMGIsal^-r z8s|FBeO%DEY2%iUEA)Ns_ZP9yy&K~&8uH)?%bkc;cQNd{N!yJ)>?V&O4}Vi$eKm#UkE@%1WQ1J)hAfzg&PbieDV-qBri zpW@!?zT5p5_g|3*&2xmOlc%SrFY*xX8EehMJ;=k}0eM)g^58fi4+Sa@n3*I?kP8FG z#*g8TkH=}2QC&FTOsP_WARl zKXvxWvyW@q=i#6Ge(rs?`rNY5?arM$H~H+Ub77kHS?#%ibH3+XKAZH}xX;FY(lq2g zmPYw{TMccc20uyDzQ*5==tDI6C{mHr{g;PuYbDf{{`v=V9XLN^DXGFH^T?I z27Oo^YV{hlhQ+8G8(~>_XnWewmn=oExeEPlJ^INal7rs=I<$&+(i-%cbJ73ip!Q#{ z-Hx?NTeT!|jkbZ-!b4hyeq}9eG!f@CuRse?K#EZBl1Ul%`AR|WwG4j7PP8Tupv}1t zKEz&l^N+yaeggi_QM7$8pwE2^eec`w8a_o&(gF|uJM9~I2j9X2o~g|uMC;aihyfch z=(Kb)gp4FZi6ag*@FH%+gLrBg@a;|T_QJ_TSXL{EA=5}QNhN7yu9iu%wR;FfpKQ_Y zB}{t=zQ;adOY7(=Z4Vi&JxuJh{lp%<%P8$pG7P=@2<;GY(jJ3{@CjStM)ZX(!L^z+C`G7{YYkL7f6csGu9{kK&ERykqr1@v$Q|RJnc{IZ?ZtU zLKbP4$wE@4eSlf!?=gRQKhbLs5*v)i#=?gwz<1FY3C31TOG!S?@oS{-kk#~E`W}6s zG?6v*GkTVuBhB=4`UP1_zoh3$3;l|;(ytK_eM2vhcKRP2$X?Z|vKQHL7KKx!KWAUCm)Hrarv~;i zdxhH2L3A*ClMbP_>?}J+htgr}WA+JpqSx4`>@(`XQdlxeWoc|0 zOJ~!mBOT6OXD3-MTTVyNk<85U*q7`)9mVq5SFC^)YQMoF{)J>|zmnP7Ut~U7+fw#5 z4MSgEu2s-*?7UY6uiyrF{Dm|^%V$Su6iv{Y50ReEM|E3x3o-(=5QU`U{WWjn~kH#ywgEpGpr0yNHAymDK=GzV}v!G9~J3oD1-P`xWaRmG%JT9(_ zM)>=sXctS+N_t|XzzFXr5_gErwL)Cy04ooFwfOTy+f<1dncvOAdzENg3*qJSHxtmt z*5aK?z-y2W5EN^{p$7dq2C~7URAAJBUI^Z*M2T={C4R*ypF((+oO&(Z2o= zDcX~f`08x5=7bdmE?C<#2C>#f?HBEmb|<`qUoocgJKCe&7>RfcEzb|==cu+3{k9E! zlur09_oIi?p&xq=EAH<>3v)MS=^ZsUthC>YNU;l^zk~2Ch9LTQ3enziv{MJrTN}{t z@$)&Rz&BY2zvl>ikD=O67{B-#BN(3WYaYNlUN4NlY)8B|9Ah`nV(%e7o^kGy3~Mh^F%3M+_!Iw0yKO`{7Z*Ux6QF4?krXw)Alzju^>& zjSPoBFaqAoDEKyiVm!%-IK#s@iI(vcqUXQiA&eno;nTQtuLj=DIO2`I--r0ZrznEo z;E(ZwV(o1bKmy@Y+z6lI5fV%$kPs4zR`)VIpfC~+k0Jt|REhQ;d?M_Sg|Vv`^!F3V zB+S81*1m&Z@eG*?ui|TxfU&DY)VVV48jNQoYZW9#D<`R%1x8W{?`ax5uPTxb?*jE7 z{>Dm@L1vOn^dwni7DiO(XtiW6JjQyhj?5$3WIo0}7m`IJ2cF;(crHuHGLj1~vXLxD z^s+`8L0)i~v$7F$QH zf(KI%Zw!8l_6=E$+Oh>}>OREC*2nP9uY>1!7CuuG#y`%HHL&@I;PLL!-Y3l%m2Dxd zq>Z$5Z-sP{^`whzAREah_(Io`&14I?j$F@u6^tiay%p}OklV=Z*y%a={ zcawXhUqT)r50Zz-9R8fRX5*5Eot~zi^+W*H__w3inO8U((+v;r@s8J-Fw={SEGGPzUZ&a8H7c zqNAx3VoVq6ifDN(=Iq@u3hqh0=s1ew_^2=Sqy97iCm)WdK{S|7pdmDr8fh2}rx6&_ zj)Wf(O=D;*cGH_gwyXbCu7rmR_L+_>c z(fjEG^g;R%-9z`%ee_|vpFV;(@iF>1eS$to574LRL3)TjjhOHmj66I?kIS zJ#+dM{hIsa^gma7PD;v}6LBn_UV&gDs;=_EIAM3M+6~?ks+rjQ+JJ~L_8zV(`W1Qt)b|1SRdyqYd)hv71Ubc@t%=WWKFpBgTdz?MNo@58u zQ|ur+gi)o#>>2hfdyXC9aRZMPL~MYVfX4&E)8`&Nk1cpS@Fqr}-bP%4n1ja`A{OBh z0ippOA&7Va9zVSOuh}>30{aj9)^0^{bzxyenYp5%G+$qnUs7+@m6cYT4ON9Tg|%iw zd1*ymZK0v2Fu$^*z>ttvUAU^yCZXJ%UtL*YlTcY)Sy8xRNJ4dKMX@;_h&Bl+;%b|e zUs|1CS6)PmC1Aw|$IqzK@Ul!D4ybAEndMXgPWq{>n%ifvLPW!tnq zGInYCmF4ATiL9HJXRfxL*7wwqE=5aMqNPg$hV3x`XGy2gnWC(E$8A2Mn zjDC-F84%5oDW7M`=b8PU8?ppPeO5_jb%lLaNgY&BT~}UauB)}ll2X`a)s&fQN+gLn z>LMx4=}&2{cw{@bPe|Q7Mr8R~H#nt9jg|>_O9vT)2kReCBHYCSdG6u8MtywWMt713z6}!1m^2+Z= zu0Tp(C|))ciqw!oD^;69NsbkwybD!%7x%qySKLoUx)NUAC4El~rBbF+B~z&??^0Rb z*Ysi9t>{O`P$uLwl*v*k>-R`k#>=}xKCh6^EBZY*RLWwg6va?EpcpEp6tLjB&!KiKkqdJw{I#u3vqP**5dDly_^?hUq*IUcG zUY2)b-$O&AEbrCgwIQpmB{K+jd4*+_^|t+lrs>mZ={5SY%8KF|o4SfpV|YZgyrxDB z7KDJDH(1aTcS0t9rz@$fTp>^+jH&wC%8JSw`(Cj`YOuMYvbM0Su+*$iGoy;@=aulL zC&OHxS74?KOX;jq{oK;xaxQzZk_YM&&O?@r_Y@V-(#OMK@N_ zja77G6`fc`CsxT7tK>>h_z4O>LE$GT`~<}}LFqL?@lR0v6BT}#KS|L~Qtv0LYl@ODMah?{=%q?MM;hhxNL8OBjgoGps@IW5DOaRX<|EQ5 z`9vBe{YayfGtwyOMjCtRN_iuVQr<|T)N`a!>Nzq@@C`Rg{YDt@>cWD&GW(TvXlA(sS6x$Bz->O<7_MkW zn+8|Z)KwK$msVEmQtGNJ#p5u=5@+VhbXe5j!kSw2xwVA_gW>uWmKK-Pme|#nz=xFg zHMT{it9tM4YQU^QJ>V2lQ&L6dQ&Urga&S!^(bpqcyNuy|Fy49aUZfwIwYTX9v-Uef zMdxWnam2g5PGdFt0r;{BAvNXsRfa@)Ns^ajc}bC%RC!60mud2nE-%yNWrnvgqX`}hou!(mm?U+E2|Mu zUTUx?X~JR3#KV+{;{bZ`JS;rSw!&OhSyNkGSyfUfVBxAe3lEdkBRou2kMJ;AJ;I{G z?Ko{*`xu2qMcB$PLBIy3)Ru^w!Q34dceV+lXw|(?gt&J|=rbAhP~=?P4^8OxH$+m# zE8=!oU+5yz_DT7L1<;aNq-<0AF_VFffIFmI#cXKG00y?{{oYIOCuI87@1+lXFSAc( z+pK;xvii};x{8KvR^LlQvj)=3?nfiLAC2s*XbjCB_}-#ETssx)NrlJ&P~6+8=ubXK z>U+bXU+kwI3cps~%V1DaP!XSeAmc)DKddh*REW~uRfv7m9QwtF@?n~&F5=#?f50dp z4*dc~i7vxKac`$WNA;kOj-84n#e-q#RySFs9r{I<0^ry`yp*(Lj47Y?k1*BKzU&O< zA*Y1OxKqBx{SS6Pew z+yRwjZok)L#45?jC{*0rsW??V=+o^G5wHsI@BxvndaYl%JM@cV#Z!lVv8wd14II?kAx0R=N$)+TcGSxKJphC$67iuBtNAnRPU?4uc1R2b)t;$>t$6QEXUJ zNhwURkMuD0pqJGU5fw?%R3s!4gTP7L4CaD~yOAo~>3vlI^((j#HkFdSXv8BDlW0X9 z%LnKFnHqN0>vrlLLB>u!6p0}sjuN+mph-G8Mw4=~Ov<@3Dd)kxw$6gj+!Es#F0uf(JCFSB#l;*MJvf- zm1MCh=LyPUpx#P_in4WTmNOB~P-F zCt2}NR`Mh(Ig*v;l2yKvm0Zb+Z;IlRqWGjJJ}HV%isF-^_@tIoMLuyA`NUQ5#Z~adRpb*_kxyJjK5-Rt;41QotH@_qN}?!F zTowOB#XnK;mth{BEB=X!f1=_q!#+G${1X-bM8&_?-evd)JjFjz@t5`%mLly9SH)k3 ziAbybCn^3(ioXmS@m%FUN%2oo{F4;_B$fXpmH#BgU)pn6inM23RsN+tBdzi;?HOss zU)nR$D*rNEMOyKf_KmdSFYOv>#b4Sr(klNl%neJCVJ@yJ|I*HpR{589jXp(k@tKfsH;A1q&^21ftQz-}PlX@Pj@MBe4+7;ec z^;g=T(G;Wj#V9&aLcfr+_p0QJ6?y?&y&t3U6QlAY?F;fKxU?sv)qB!Dk?zfhYA0pA zg4_yEM$*PeIjk6N42#5S4Zb(YFtKW5G}bE&!JVH*c({vI8oK1vjCooBLccOC0JA>R@HbbPr7=UBf$hXR>3Ynq zc3{TzE-}N}g;~>kG0%Dr=1to$*ZKfvP5FH5gP3`}A9JYfm_gku=1_0M3@V?2eFAf_ zSapjzRXLY>2(zhtF7;W=rrw0P*&~>py$!RpM=`T{E9O;Sz|884m{WZTbF1qxvw8>S zRr###?U-AA6|<|aW3Kfy=5Xa4>)V)R{r@lJ)iVCymav7kU|oYN{vuJ1d~Lr8D{W>W zwG(SsqOjuODXeWdgmo*KSlx08>u=t`s@i<9MxqSsBfi60rysD^rWtE(E@4f}?^q|g z4l6|s+IlhwYf|{PPOey+F&1k}?!&qg6V}3F^$1qHP9)Q`qhvZ(ew@a-k6BoOG8e0A z-@$5+McVtANnfjdgt_!qti*u5V$}>^aWqo=-$$eM7*OJdS6XW>!|E7(8-Nu$9H3!+ SOfW83od>JLehM6 state val frame : state -> width:float -> height:float -> elapsed_seconds:float -> I.t + val clear_color : Color.t end let load_font name = @@ -45,7 +46,7 @@ let run (module T : S) = ~w:window_width ~h:window_height "SDL OpenGL" - Sdl.Window.(opengl + allow_highdpi) + Sdl.Window.(opengl + allow_highdpi + resizable + windowed) with | Error (`Msg e) -> Sdl.log "Create window error: %s" e; @@ -74,7 +75,10 @@ let run (module T : S) = let prev_frame_fps = ref 0.0 in let freq = Sdl.get_performance_frequency () in let font_sans = Lazy.force font_sans in + let start_ticks = Sdl.get_ticks () in while not !quit do + let window_width, window_height = Sdl.get_window_size w in + let ow, oh = Sdl.gl_get_drawable_size w in let timing_start = Sdl.get_performance_counter () in while Sdl.poll_event (Some event) do match Sdl.Event.enum (Sdl.Event.get event Sdl.Event.typ) with @@ -82,13 +86,19 @@ let run (module T : S) = | _ -> () done; Gl.viewport 0 0 ow oh; - Gl.clear_color 0.3 0.3 0.32 1.0; + Gl.clear_color + (Color.r T.clear_color) + (Color.g T.clear_color) + (Color.b T.clear_color) + (Color.a T.clear_color); Gl.(clear (color_buffer_bit lor depth_buffer_bit lor stencil_buffer_bit)); Gl.enable Gl.blend; Gl.blend_func_separate Gl.one Gl.src_alpha Gl.one Gl.one_minus_src_alpha; Gl.enable Gl.cull_face_enum; Gl.disable Gl.depth_test; - let elapsed_seconds = Int32.to_float (Sdl.get_ticks ()) /. 1000.0 in + let elapsed_seconds = + Int32.to_float (Int32.sub (Sdl.get_ticks ()) start_ticks) /. 1000.0 + in let () = let width = float window_width in let height = float window_height in diff --git a/benchmarks/lots_of_text.ml b/benchmarks/lots_of_text.ml index 36e45ab..ba89798 100644 --- a/benchmarks/lots_of_text.ml +++ b/benchmarks/lots_of_text.ml @@ -76,3 +76,4 @@ let frame { font; commands } ~width ~height ~elapsed_seconds = ;; let name = "lots-of-text" +let clear_color = Color.white diff --git a/benchmarks/main.ml b/benchmarks/main.ml index d385636..6f81f10 100644 --- a/benchmarks/main.ml +++ b/benchmarks/main.ml @@ -1,4 +1,6 @@ -let benches : (module Bench.S) list = [ (module Lots_of_text); (module Many_graphs) ] +let benches : (module Bench.S) list = + [ (module Lots_of_text); (module Many_graphs); (module Source_code) ] +;; let print_usage argv0 = print_endline "Usage:"; diff --git a/benchmarks/many_graphs.ml b/benchmarks/many_graphs.ml index 36a5ae4..e1186c7 100644 --- a/benchmarks/many_graphs.ml +++ b/benchmarks/many_graphs.ml @@ -139,3 +139,4 @@ let frame () ~width ~height ~elapsed_seconds = ;; let name = "many-graphs" +let clear_color = Color.black diff --git a/benchmarks/source_code.ml b/benchmarks/source_code.ml new file mode 100644 index 0000000..e5f72b1 --- /dev/null +++ b/benchmarks/source_code.ml @@ -0,0 +1,97 @@ +open Wall +module I = Image +module Text = Wall_text + +type command = + { color : Color.t + ; text : string + ; x : float + ; y : float + } + +type t = + { font : Text.Font.t + ; commands : command list + ; text_height : float + ; text_width : float + ; kind : string + } + +type state = t list + +let init ~placement = + let font = + let tt = Bench.load_font "./RobotoMono-Light.ttf" in + Text.Font.make ~size:18.0 ~placement tt + in + let metrics = Text.Font.font_metrics font in + let source_code = + In_channel.open_text "../lib/wall.ml" + |> In_channel.input_all + |> String.split_on_char '\n' + in + let gap = metrics.ascent +. metrics.descent +. metrics.line_gap in + let gap = gap *. 1.5 in + let (text_height, text_width), commands = + List.fold_left_map + (fun (y, width) line -> + let cmd = { color = Color.white; text = line; x = gap *. 2.0; y } in + let measure = Text.Font.text_measure font line in + let y = y +. gap in + let width = Float.max width measure.width in + (y, width), cmd) + (0.0, gap *. 2.0) + source_code + in + let kind = + match placement with + | `Aligned -> "ALIGNED" + | `Subpixel -> "SUBPIXEL" + in + { font; commands; text_height; text_width; kind } +;; + +let init _ctx = [ init ~placement:`Aligned; init ~placement:`Subpixel ] + +let paint_text ?(valign = `TOP) ?(halign = `LEFT) ~x ~y ~color ~font s = + I.paint (Paint.color color) (Text.simple_text font ~valign ~halign ~x ~y s) +;; + +let rotate_around ~x ~y ~a = + Transform.translation ~x ~y + |> Transform.rotate a + |> Transform.translate ~x:(-. x) ~y:(-. y) +;; + +let frame state ~width ~height ~elapsed_seconds = + let _x_offset, regions = + List.fold_left_map + (fun x_offset { font; commands; text_height; text_width; kind } -> + let matrix = + let t = (Float.cos (elapsed_seconds /. 5.0) +. 1.0) /. 2.0 in + let t = if t >= 0.8 then 1.0 else t *. (1.0 /. 0.8) in + Transform.translation ~x:x_offset ~y:(-.(t *. (text_height -. height))) + in + let image = + let code = + List.map + (fun { color; text; x; y } -> paint_text ~x ~y ~color ~font text) + commands + |> I.seq + |> I.transform matrix + in + let label = + paint_text ~x:(x_offset +. 5.0) ~y:(-. 5.0) ~color:Color.white ~valign:`BOTTOM ~font kind + |> I.transform (rotate_around ~x:x_offset ~y:0.0 ~a:(Float.pi /. 2.0)) + in + I.stack code label + in + x_offset +. text_width, image) + 0.0 + state + in + I.seq regions +;; + +let name = "source-code" +let clear_color = Color.black diff --git a/benchmarks/source_code.mli b/benchmarks/source_code.mli new file mode 100644 index 0000000..2c850be --- /dev/null +++ b/benchmarks/source_code.mli @@ -0,0 +1 @@ +include Bench.S