-
Notifications
You must be signed in to change notification settings - Fork 3
/
blog.html
1315 lines (835 loc) · 143 KB
/
blog.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset=utf-8>
<title>Blog</title>
<link href="/css/grey-and-blue.css" rel=stylesheet>
<meta name="theme-color" content="#3377FF">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5.0, minimum-scale=1">
<link rel="canonical" href="https://ddr0.ca/blog">
<link href='/css/blog.css' rel='stylesheet'>
<link rel="alternate" type="application/rss+xml" title="DDR's Blog" href="/blog-rss-feed.xml" />
<link href='/css/prism-okaidia.css' rel='stylesheet'>
<script defer src='/scripts/unindent-code-blocks.js'></script>
</head>
<body>
<a href='#content' id='skip-nav'>skip nav</a>
<div id='content-holder'>
<div id="content-header">
<div id='badge'><img alt='' src='/images/icons/page.svg'></div>
<h1><img alt='blog' src='/images/text-blog.png'></h1>
<div id='icon-bar'>
<a href='/blog' class='selected'><img src='/images/icons/page.svg' ><span>blog</span></a>
<a href='/gallery' ><img src='/images/icons/work.svg' ><span>gallery</span></a>
<a href='/contact' ><img src='/images/icons/chat2.svg'><span>contact</span></a>
<a href='/rss.xml' ><img src='/images/icons/rss.svg' ><span>rss</span></a>
</div>
</div>
<a name='content'></a>
<div id='content-pane'>
<h2 id="a-visual-emoji-tester">
<a href="/blog-posts/21.A_Visual_Emoji_Tester">A Visual Emoji Tester</a>
</h2>
<p>A few months ago I got massively sniped by <a href="https://spiffy.tech/is-this-an-emoji">https://spiffy.tech/is-this-an-emoji</a>. I wrote up a different, more reliable approach (although more computationally intensive) to answer the question, "is this string one single colour emoji".</p>
<p>Presenting: The <a href="/side%20projects/visual-emoji-tester">Visual Emoji Tester</a>.</p>
<p><img width=407 height=38 alt="A screenshot with the word input, then an input with the colour emoji slightly smiling face, an arrow to the same emoji but rendered smaller and fuzzier, and then the verdict, Is One Colour Emoji: True." src="data:image/png;base64,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"></p>
<p>The approach the Visual Emoji Tester takes is to draw the text in question to a canvas, then see if there are any visible spaces in the resulting image. If there are, then we've got multiple characters. Similarly, if there's no colour in the resulting image, then it's not a colour emoji. The process is fairly future-proof, too - it will never go out of date as new emoji are released and incorporated into your font!</p>
<span class="tags">tags: <a href="/blog-posts/tags#web-dev">web dev</a>, <a href="/blog-posts/tags#emoji">emoji</a>, <a href="/blog-posts/tags#toy">toy</a>, <a href="/blog-posts/tags#tool">tool</a></span>
<hr>
<h2 id="salty-pepper">
<a href="/blog-posts/20.Salty_Pepper">Salty Pepper</a>
</h2>
<p>Let's briefly look at the $100 <a href="https://www.zwilling.com/ca/zwilling-enfinigy-electric-salt-and-pepper-mill-black-53103-701/53103-701-0.html">Zwilling Enfinigy Electric Pepper Mill</a>. Stacking it up against the $7 electric pepper grinder I bought about 5 years ago by mistake, for which this was the replacement, I have some serious complaints. Zwilling ain't the brand they used to be, it would seem.</p>
<h3>Why is it bad?</h3>
<p>It can't grind while it charges, and if it discharges you can't run it manually. This requires you to own a back-up pepper grinder, because you usually don't need pepper in about 20 minutes or so. Even when the mill is charged, it mills pepper quite slowly. I like flavour, so I'm usually waiting around for a minute or so, or much longer if I'm cooking something. However, the kicker is that the mill is <em>wildly</em> breakable. Mine produced more e-waste than it ever did ground pepper, even including my one successful repair.</p>
<h3>What broke?</h3>
<p>After about a dozen grinds, less than one full charge of the battery, the button on the side got stuck in the "in" position. There was an audible snap, and the mill wouldn't turn on any more. I put it aside for a few months, after which I dug in and fixed it. It was quite fixable though! After some mild disassembly, a little plastic needed to be shaved off before the button wouldn't catch any more.</p>
<p>However, about 5 grinds later, the mill wouldn't turn on again and battery wouldn't charge. No measurable power draw when plugging the mill in, just complete failure. So, I had recycle what is effectively a new Zwilling pepper mill. Compare this to the $7 el-cheapo one which lasted 5 years, was repeatedly dropped, and never gave me any trouble at all until the plastic gear train wore out.<p>
<span class="tags">tags: <a href="/blog-posts/tags#pepper">pepper</a>, <a href="/blog-posts/tags#salt">salt</a>, <a href="/blog-posts/tags#small-appliance-repair">small appliance repair</a></span>
<hr>
<h2 id="negative-results">
<a href="/blog-posts/19.Negative_Results">Negative Results</a>
</h2>
<p>A few months ago, I decided to try my hand at writing a little bit of multithreaded Rust, running in Web Workers. While I have not been met with success, I feel it is important to mention what I've done so others can save some time and sanity – plus, I think the story is at least mildly interesting. All code is open-source and all examples should be fairly easy to run.</p>
<p>Besides, if nothing else, you can take great <a href="https://www.youtube.com/watch?v=kb20xhcrK4g">schadenfreude</a> in my hobbies.</p>
<h3>Backstory</h3>
<p>It's currently June 2023. In November 2020, I decided, hey, you know what would be cool?</p>
<p><em>A multithreaded <a href="https://sandspiel.club/">falling-sand game</a>.</em></p>
<p>So, naturally, the tools I choose to do this with are <a href="https://developer.mozilla.org/en-US/docs/Glossary/HTML5">HTML5</a> for the front-end. It's easy to package, I just have to throw a couple files up on my web server and there we go. Then, for the core, I'll write the simulation in Rust, and run it on all the cores of my CPU with a shared-memory model. I know we've got <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics">atomics</a> in JS and WASM now, so let's try to use it to build something simple. Heh. <strong>Heh,</strong> I tell you.</p>
<h3><a href="https://github.com/DDR0/Stardust/tree/attempt-1">1<sup>st</sup> Attempt</a> - wasm-bindgen and Why FFI Is Not Your Friend For Speed</h3>
<p>This approach attempted to use <a href="https://docs.rs/wasm-bindgen/latest/wasm_bindgen/">wasm-bindgen</a> to read/write shared memory in the web-worker from <a href="https://webassembly.org">WASM</a>, generated from Rust, in a fairly naïeve manner. But why not just set the memory directly from WASM?</p>
<h4>The Core Problem</h4>
<p>Web assembly has one linear memory.¹ This is, by default, not shared. And if it is shared, <em>all</em> memory is shared - you can't just share a portion of it.</p>
<p>To share the memory, you also literally have to share the memory. Generally, <a href="https://github.com/DDR0/Stardust/blob/897ae96ab8a5d8957e97d0637abc26ffe69dc8cd/www/worker/logicWorker.mjs#LL4C12-L4C12">you initialize your WASM in a web worker</a>, the memory for the worker is allocated there. So, if we want to have shared memory, we must send it to the worker in a message and pass it in to whatever is compiling our WASM. This isn't, or at least wasn't in 2020, supported by wasm-bindgen as far as I could tell.² So, what do we do?</p>
<p>To get something working, I simply called back out to Javascript whenever I wanted to modify a value in the shared memory. The way this ended up working out was that, to read, say, the x-velocity of a falling particle of sand, I'd go something like <code>Reflect::get_u32(Reflect::get(Reflect::get(world_obj, "velocity"), "x"), particle_index)</code>, which is a fairly direct translation of JS' <code>world.particles.x[particle_index]</code>. I don't think the idiom translates very well, especially including that this rust allocates two strings and a number as part of the FFI operation which then needs to get GC'd. I did try <a href="https://github.com/DDR0/Stardust/blob/897ae96ab8a5d8957e97d0637abc26ffe69dc8cd/crate-wasm/src/particles/particle_data.rs#L21">allocating the strings statically</a> to speed things up, but it didn't speed things up that much. I could peg every core of my computer and hit, maybe, 5fps doing nothing. So this approach was, as suspected, hot garbage and FFI is slow when you're doing a few million operations a second.</p>
<blockquote>
<p>In total, the render thread takes 128ms on Chrome and 59ms on Firefox to render out a 300x150 playfield. Aiming for a 120hz framerate, that gives us a budget of 8.3ms/frame, which is not particularly in the neighbourhood we need. Effectively all the time is spent creating and destroying our internal representation of a particle, which consists of world, thread_id, x, y, and maybe w/h if it references a real particle. The allocation and deallocation of one of these data structures takes about 30% of the processing time, and we usually end up creating a few of them as they're what we use to work with other particles as well. Another 20% of the time was spent reading data from the JS side of things, since we can't map the data we're working with in from the shared array buffer passed to the web-worker.</p>
<p>Rust interop with JS in this case has also proved rather awkward; while I'm sure it would work for other projects, for the sort of high-performance access we're looking at it's not suitable. Right now, WASM is more suited to the sort of workload where a little data is passed in to do a lot of work on, rather than a lot of data passed in to do a little work on.</p>
<p>One alternative might be to copy the raw memory in to the WASM process in the worker thread, thus avoiding the lookups. More sensibly, I think the best solution is just to avoid using WASM for this at all, and use Javascript or Typescript in the worker.</p>
</blockquote>
—<a href="https://github.com/DDR0/Stardust/tree/attempt-1#outcome">Attempt 1's readme.md</a>
<p>Oh, and the kicker? Back on the main thread, it seems you can't paint shared array buffers directly to canvas - you need to copy them into a new ImageData() first, because ImageData will only accept non-shared array buffers. So our zero-copy goal is kinda hosed at this point, if we're being <em>pure</em> about it. Let's ignore that and continue on. It's certainly not an ill omen of things to come... right?</p>
<h3><a href="https://github.com/DDR0/Stardust/tree/attempt-2">2<sup>nd</sup> Attempt</a> - Can't Read That Here</h3>
<p>This was a fairly intense yet short-lived branch, because I ran up square against the core problem mentioned above. Diving in, this was when I figured out what was happening, and why I couldn't pass in a chunk of shared memory directly as I'd first assumed I could. Or, rather, I can <a href="https://github.com/DDR0/Stardust/blob/attempt-2/worker/sim.mjs"><em>pass it in</em></a>, but I can't <a href="https://github.com/DDR0/Stardust/blob/attempt-2/worker/sim.rs"><em>read it out</em></a>.</p>
<blockquote>
The issue is that [multiple linear memories are] not a value which is represented in linear memory. That thing which Rust and C++ are based around. So it's kind of a new concept for them, and they just... don't support it, according to <a href="https://github.com/rust-lang/rust/issues/60825#issuecomment-566273568">this GitHub issue from 2019</a>.
</blockquote>
—<a href="https://github.com/DDR0/Stardust/tree/attempt-2#outcome">Attempt 2's readme.md</a>
<p>So, now we know what we're up against, what do?</p>
<h3><a href="https://github.com/DDR0/Stardust/tree/attempt-3">3<sup>rd</sup> Attempt</a> - Can't Pass Array References</h3>
<p>Yeah, that doesn't exist as a concept. You can't pass arrays from JS to WASM, because WASM only works on what is in linear memory. The array isn't in linear memory because we didn't copy it there, and we can only invoke functions and provide numbers as args to them.<p>
<p>A few months burned reading refactoring, moving on.</p>
<h3><a href="https://github.com/DDR0/Stardust/">4<sup>th</sup> Attempt</a> - Memory Synchronisation Issues</h3>
<p>This brings us up to today, in mid-2023. I've managed to make my Rust generate - at least theoretically - with <a href="https://github.com/DDR0/Stardust/blob/multithreading-issue-reproduction-3/worker/.cargo/config.toml">shared memory multithreading support</a>, by adding <code>atomics</code> and <code>mutable-globals</code> to the feature list and linking with <code>--shared-memory</code> on Nightly. <code>bulk-memory</code> and <code>--import-memory</code> allow for <a href="https://github.com/DDR0/Stardust/blob/885be83b8b4475e6811bc85ebcbadd30f1c9d8c4/worker/sim.mjs#L14">the import of our shared memory object from JS</a>. Coss'… this doesn't actually <strong>work</strong>. And I have no idea why! The documentation I've read says it darn well should, but it doesn't. My threads are sharing memory <em>too well</em> now - <a href="https://stackoverflow.com/questions/76452839/how-to-compile-rust-for-use-with-wasms-shared-memory">non-shared local variables appear to be getting allocated over top of each other</a> in shared memory.</p>
<p><strong>And this is where I give up. I can't figure this out. Save yourself some time and learn from my mistakes, and avoid using multithreaded Rust on the web. Even if someone hands you a fully-baked module, it's more trouble than it's worth - you'll wind up fiddling with it when something inevitably breaks, like browsers starting to require site-isolation headers. It's under-documented and very few people understand it. Certainly none than I can find asking around on various forums and Discords over the years this has been ongoing. You will not be able to get help when things go wrong, and things will go wrong.</strong></p>
<p>On the upside, it wasn't a total loss - <a href="https://github.com/DDR0/Stardust/wiki/External-Bugs-Filed-As-Part-Of-This-Project">we filed a few browser bugs</a>. But at the same time, that shows no one's been poking around this area much.</p>
<hr>
<p>¹: There's loose plans to allow multiple <a href="https://developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/Memory">memory objects</a> to be provided to WASM, but that is not high priority because <em>nothing expects to operate on more than one memory.</em> We have based all our technology on things which can be pointed to with a numerical pointer, and web assembly memories are <em>named</em>, not numbered. So currently, we only have one memory, which is the default the number points into.</p>
<p>²: wasm-bindgen does have <a href="https://rustwasm.github.io/docs/wasm-bindgen/examples/raytrace.html">a proper mechanism to multithread things</a>, but as far as I can tell it works by copying memory around which isn't what we want. If it does share memory, I can't figure out how.</p>
<span class="tags">tags: <a href="/blog-posts/tags#web-dev">web dev</a>, <a href="/blog-posts/tags#rust">rust</a>, <a href="/blog-posts/tags#wasm">wasm</a>, <a href="/blog-posts/tags#multithreading">multithreading</a>, <a href="/blog-posts/tags#html5">html5</a>, <a href="/blog-posts/tags#negative-result">negative result</a></span>
<hr>
<h2 id="managing-files-on-a-phone">
<a href="/blog-posts/18.Managing_Files_On_A_Phone">Managing Files On A Phone</a>
</h2>
<h3>The Problem</h3>
<p>Let's say you've accumulated about 20,000 files on your phone which you would like to move to a computer. The bulk of them are photos, but there's some stuff you've downloaded using multiple chat apps as well. In short; you've got yourself a non-homogenous mess.</p>
<p>There's a few options you have. You know you can send yourself stuff by email or a chat app, but those have a file size limit. You could work around that with dedicated service like <a href="https://wormhole.app/">Wormhole</a>, but it still has limitations. Besides, just sending the files wouldn't help you organize your phone. And the on-phone apps are struggling with your photos folder.</p>
<p>How about cloud sync? If you've got Apple, in my experience, you're going pay some money, sync for a month, and find a perfect duplicate of your files labelled "failed to sync". There's also the worrying question of "is it syncing my full phone to my empty computer, or my empty computer to my full phone?" If you're on an Android, I do not think it is ideal from a privacy perspective either. Like with gravity, what goes up to The Cloud will come down from The Cloud at some point. A company gets hacked, goes bust, sells out. Or someone gets access to your account by scamming their support.</p>
<p>Besides, you probably don't have the world's best internet uplink anyway. Your telecommunication infrastructure was designed for cable TV, and your internet providers enjoy a sweet a monopoly and have no reason to offer a decent service.</p>
<h3>My Solution</h3>
<p>I ended up solving this problem for myself by installing an app on my phone, an FTP<a id="mfoap-fn1-ret" href="#mfoap-fn1">¹</a> server. Then I could use another program on my computer to manage the files on my phone via FTP; move them around, and copy them off. Here's how I did it in detail. The software used is not important, as it's nicely modular and you can substitute other software at any step.</p>
<h4>On The Phone</h4>
<p>I grabbed Banana Studio's <a href="https://play.google.com/store/apps/details?id=net.xnano.android.ftpserver.tv&hl=en_CA&gl=US">FTP Server</a> for the phone part of this. It seems a respectible one, though it's always hard to tell with these things. (If you use an iPhone, there are apps for that as well. e.g., <a href="https://apps.apple.com/us/app/ftpmanager-ftp-sftp-client/id525959186">this one</a> I found on the first page of Google.)</p>
<p class=noindent>
In the FTP Server app, I've configured a user for myself like this:
<a href="blog-posts/18.Managing_Files_On_A_Phone/Screenshot_20220903-031758.png"><img src="blog-posts/18.Managing_Files_On_A_Phone/Screenshot_20220903-031758.png" width=432 alt="Username: ddr, Password set, show hidden files, and the path / is /storage/emulated/0 for this account. Writable."></a>
Then, starting the server and connecting to the address given at the top:
<a href="blog-posts/18.Managing_Files_On_A_Phone/Screenshot_20220903-031807.png"><img src="blog-posts/18.Managing_Files_On_A_Phone/Screenshot_20220903-031807.png" width=432 alt="On the home screen of the FTP Server app, we see a button labelled stop, to the right of some text saying that the FTP server is listening on addresses ftp://<user>@10.0.0.127:2121. There are no active sessions. Some configuration and warnings appear at the bottom, but they are not relevant for this."></a><a href="blog-posts/18.Managing_Files_On_A_Phone/Screenshot_20220903-032037.png"><img src="blog-posts/18.Managing_Files_On_A_Phone/Screenshot_20220903-032037.png" width=432 alt="After connecting, there is one active session — ddr — who we specified on the user screen earlier."></a>
</p>
<h4>On The Computer</h4>
<p>
To connect to the address on the computer, we need an FTP client. <a href="https://filezilla-project.org/">Filezilla</a> is a good one for Windows, Panic Inc.'s <a href="https://panic.com/transmit/">Transmit</a> is a good one for Mac, but since I'm using KDE on Ubuntu, my default file manager <a href="https://apps.kde.org/en-gb/dolphin/">Dolphin</a> (the equivalent of <a href="https://support.microsoft.com/en-us/windows/windows-explorer-has-a-new-name-c95f0e92-b1aa-76da-b994-36a7c7c413d7">Windows Explorer/File Explorer</a>) just comes with one.
<a href="blog-posts/18.Managing_Files_On_A_Phone/phone-files-1.png"><img src="blog-posts/18.Managing_Files_On_A_Phone/phone-files-1.png" width=657 alt=""></a>
</p>
<p class=noindent>
Plugging in the <code>ftp://</code> URL we got from the FTP server app,
<a href="blog-posts/18.Managing_Files_On_A_Phone/phone-files-2.png"><img src="blog-posts/18.Managing_Files_On_A_Phone/phone-files-2.png" width=657 alt="into the address bar of Dolphin,"></a>
and entering our password, we are now browsing our phone and can move the files around as desired from our computer.
<a href="blog-posts/18.Managing_Files_On_A_Phone/phone-files-4.png"><img src="blog-posts/18.Managing_Files_On_A_Phone/phone-files-4.png" width=657 alt=""></a>
Since the files aren't leaving our immediate vicinity, this is also faster and more secure than a cloud-based solution can be. 🙂
</p>
<p>
As a fun bonus with KDE; through the magic of FUSE and having a terminal integrated with my file manager, I can also run basic Linux commands against my phone filesystem now. For example, to roughly count the number of pictures I took since I last cleared pictures off my phone:
<a href="blog-posts/18.Managing_Files_On_A_Phone/phone-files-6.png"><img src="blog-posts/18.Managing_Files_On_A_Phone/phone-files-6.png" width=657 alt="I can run "ls -la | wc -l" in the embedded terminal."></a>
</p>
<h3>Further Remarks</h3>
<p>There is one other option I haven't touched upon - <a href="https://kdeconnect.kde.org/download.html">KDE Connect</a> offers a nice file browsing feature among its many other useful features. (And it is cross-platform! Not just for Linux and Android.) However, there is currently one issue with it - KDE Connect can't let me manage my phone downloads folder "for security reasons". This puts it out of the running for this article, since managing my downloads is half of what I need it for. However, it has worked in the past, continues to work partially at the moment, and may work fully again in the future. So it gets an honourable mention, and it requires much less configuration than the FTP server option I've went with in this article.</p>
<p>For all that Linux has a reputation as hard to use, I find in some ways it's far easier to use than Windows or Mac. Today, because Dolphin supports SSH as well as FTP for browsing out of the box, it let me copy a screenshot from my phone directly to my web server using the standard graphical interface I'm used to. I've got both locations bookmarked, and having everything <em>available</em> under a standard point-and-click interface makes things so easy.</p>
<p>And if anything breaks? It's all discreet software components, you can switch them out for a different component if need be. Files on a disk are pretty much the universal language of data storage, and FTP is a pretty universally available transfer mechanism for them. 🙂</p>
<br>
<p class=noindent><a id="mfoap-fn1" href="#mfoap-fn1-ret">¹</a>: When I say FTP, I'm including SFTP in it. Like with HTTP/HTTPS, the S stands for "Secure". I'm not too concerned about security for this setup, because I'm going over a local-area network. It should be reasonably free from snooping as it's all physically within about a meter of me here. If you're routing your FTP traffic over the internet, you should absolutely make sure you're using SFTP vs FTP. Any FTP software worth its salt will support both protocols. <a href="#mfoap-fn1-ret">⮌</a></p>
<span class="tags">tags: <a href="/blog-posts/tags#mobile-device">mobile device</a>, <a href="/blog-posts/tags#phone">phone</a>, <a href="/blog-posts/tags#files">files</a>, <a href="/blog-posts/tags#file-management">file management</a>, <a href="/blog-posts/tags#networking">networking</a>, <a href="/blog-posts/tags#linux">linux</a>, <a href="/blog-posts/tags#dolphin">dolphin</a>, <a href="/blog-posts/tags#ftp">ftp</a></span>
<hr>
<h2 id="arduino-multi-println">
<a href="/blog-posts/17.arduino_multi_println">A Variadic println() for Arduino</a>
</h2>
<p>The Arduino ecosystem provides <code>Serial.print(x);</code> and <code>Serial.println(x);</code>, the latter adding a newline after the value. I almost universally want to print out out a tag so I know what's what though, so something like <code>Serial.printlnMulti(x, y, z, ...)</code> would be convenient. (<code>x</code>, <code>y</code>, and <code>z</code> and so on can be any type here.)</p>
<p class=noindent>And indeed, we can make it so.</p>
<strong>debug.hpp:</strong>
<code class="prism-block language-js">
#pragma once
#include <span class="token operator"><</span>Arduino<span class="token punctuation">.</span>h<span class="token operator">></span>
template <span class="token operator"><</span><span class="token keyword">class</span> <span class="token class-name"><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>Types</span><span class="token operator">></span>
<span class="token keyword">void</span> <span class="token function">debug</span><span class="token punctuation">(</span><span class="token parameter">Types<span class="token operator">&&</span> <span class="token operator">...</span>inputs</span><span class="token punctuation">)</span>
<span class="token punctuation">{</span>
<span class="token punctuation">(</span>Serial<span class="token punctuation">.</span><span class="token function">print</span><span class="token punctuation">(</span>inputs<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token operator">...</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
Serial<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code>
<p class=noindent>Usage like <code>debug("analog value: ", analogRead(35));</code>. You can have any number of args to the function, of course.</p>
<p class=noindent>What are the size implications of this, however? C++ is big on zero-cost abstractions, so ideally the above should copile the same as:</p>
<code class="prism-block language-js">
Serial<span class="token punctuation">.</span><span class="token function">print</span><span class="token punctuation">(</span><span class="token string">"analog value: "</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
Serial<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token function">analogRead</span><span class="token punctuation">(</span><span class="token number">35</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code>
<p>Testing this in my non-trivial project, substituting one debug stanza of many, we get a total size for the fancy C++ 17 version, 279957 bytes. This compares favourably to the baseline of 279973 bytes, as we have paid a total of -20 bytes for our indescretions. A win for expressing what you want over expresning how to do it, I guess. ¯\_(ツ)_/¯</p>
<span class="tags">tags: <a href="/blog-posts/tags#arduino">arduino</a>, <a href="/blog-posts/tags#embedded">embedded</a>, <a href="/blog-posts/tags#code">code</a></span>
<hr>
<h2 id="fixing-linux-audio-ubuntu-21-22-in-spring-2022">
<a href="/blog-posts/16.Fixing_Linux_Audio_Ubuntu_21_22_In_Spring_2022">Fixing Linux Audio, Spring '22 edition</a>
</h2>
<p class=noindent>When upgrading from Ubuntu 21 to 22 this spring, I encountered two separate issues:</p>
<ol>
<li>KDE would not show any audio devices in the task bar, under the speaker icon, which appeared muted.</li>
<li>After fixing that, my bluetooth headphones would connect but immediately disconnect, announcing pairing failed.</li>
</ol>
<p class=noindent>To fix the first issue, I had to enable Pipewire. It had been installed, but not turned on.</p>
<code>systemctl --user --now enable pipewire pipewire-pulse</code>
<p class=noindent>It seems that Pipewire has now replaced ALSA, which was the previous sound system.</p>
<p class=noindent>Interestingly enough, it seems Pipewire is now also responsible for Bluetooth! To fix my wireless headphones not connecting, I had to install Pipewire's bluetooth module. From <a href="https://www.reddit.com/r/linuxquestions/comments/led200/bluetooth_headset_wont_connect_after_installing/">Reddit</a>:</p>
<code style="white-space: pre-line;">sudo apt install libspa-0.2-bluetooth
systemctl --user restart pipewire.service pipewire-pulse.service</code>
<p class=noindent>And now I can listen to my music again.</p>
<span class="tags">tags: <a href="/blog-posts/tags#linux">linux</a>, <a href="/blog-posts/tags#sound">sound</a>, <a href="/blog-posts/tags#audio">audio</a>, <a href="/blog-posts/tags#fix">fix</a>, <a href="/blog-posts/tags#tech-support">tech support</a></span>
<hr>
<h2 id="hydrating-objects">
<a href="/blog-posts/15.Hydrating_Objects">Hydrating Objects in Javascript</a>
</h2>
<h3>What is Hydration?</h3>
<p>Hydration is a step which sometimes occurs after parsing your data. In some languages, such as Javascript, the parsing of the data from source text is usually done via <code>JSON.parse(…)</code> a DOM method, or a fetch reply's <code>.toJson()</code>. However, while this converts the text into your language's data structure, it doesn't convert them into <strong>your</strong> data structures.</p>
<p>For example, take a Rectangle class. If you parse a rectangle stored as x1y1x2y2 coordinates with <code>JSON.parse(…)</code>, you don't get your Rectangle object with the nice <code>.width</code> and <code>.height</code> properties. Hydration is the process of constructing your Rectangle objects from the rectangle x1y1x2y2 data you parsed.</p>
<p>Hydration usually goes hand-in-hand with the validation of your data, because if you verify the data in one place and then construct the objects in another place, the two places will <strong>inevitably</strong> get out of sync at some point. (See Alexis King's "<a href="https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/">Parse, Don't Validate</a>" post from 2019 for more details on the matter.) To that end…</p>
<h3>Simple Hydration Method</h3>
<p>I have written up a basic 8-line method to hydrate and validate your data from parsed JSON. It lazily evaluates and validates the results, so it will work well with large data sets. The hydrated data is used exactly like a normal (read-only) data structure.</p>
<code class="prism-block language-js">
<span class="token keyword">const</span> <span class="token function-variable function">hydrate</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">structure<span class="token punctuation">,</span> data</span><span class="token punctuation">)</span> <span class="token operator">=></span>
<span class="token keyword">new</span> <span class="token class-name">Proxy</span><span class="token punctuation">(</span>Object<span class="token punctuation">.</span><span class="token function">create</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">{</span>
<span class="token function-variable function">get</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">cache<span class="token punctuation">,</span> prop</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>prop <span class="token keyword">in</span> cache<span class="token punctuation">)</span> <span class="token keyword">return</span> cache<span class="token punctuation">[</span>prop<span class="token punctuation">]</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>prop <span class="token keyword">in</span> structure<span class="token punctuation">)</span>
<span class="token keyword">return</span> cache<span class="token punctuation">[</span>prop<span class="token punctuation">]</span> <span class="token operator">=</span> structure<span class="token punctuation">[</span>prop<span class="token punctuation">]</span><span class="token punctuation">(</span>data<span class="token punctuation">[</span>prop<span class="token punctuation">]</span><span class="token punctuation">)</span>
<span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">unknown key </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>prop<span class="token interpolation-punctuation punctuation">}</span></span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>
</code>
<p class=noindent>Usage Example:</p>
<code class="prism-block language-js">
<span class="token keyword">const</span> data <span class="token operator">=</span> <span class="token function">hydrate</span><span class="token punctuation">(</span>
<span class="token punctuation">{</span>
<span class="token literal-property property">a</span><span class="token operator">:</span> Number<span class="token punctuation">,</span>
<span class="token literal-property property">b</span><span class="token operator">:</span> String<span class="token punctuation">,</span>
<span class="token function-variable function">c</span><span class="token operator">:</span> <span class="token parameter">d</span> <span class="token operator">=></span> <span class="token function">hydrate</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
<span class="token literal-property property">e</span><span class="token operator">:</span> Number<span class="token punctuation">,</span>
<span class="token literal-property property">f</span><span class="token operator">:</span> <span class="token function">fetch</span><span class="token punctuation">.</span><span class="token function">bind</span><span class="token punctuation">(</span>window<span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span> d<span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">{</span> <span class="token comment">//Or, say, await (await fetch('https://example.com/').body).toJson().</span>
<span class="token literal-property property">a</span><span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span>
<span class="token literal-property property">b</span><span class="token operator">:</span> <span class="token string">"two"</span><span class="token punctuation">,</span>
<span class="token literal-property property">c</span><span class="token operator">:</span> <span class="token punctuation">{</span>
<span class="token literal-property property">e</span><span class="token operator">:</span> <span class="token number">3</span><span class="token punctuation">,</span>
<span class="token literal-property property">f</span><span class="token operator">:</span> <span class="token string">"https://example.com/"</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>data<span class="token punctuation">.</span>a<span class="token punctuation">,</span> data<span class="token punctuation">.</span>b<span class="token punctuation">,</span> data<span class="token punctuation">.</span>c<span class="token punctuation">.</span>f<span class="token punctuation">)</span>
<span class="token comment">//1, "two", Promise{&lt;pending&gt;}</span>
</code>
<p>Here, I've hydrated some built-in object Javascript has, but you can use any object you have on hand of course. Note that because the hydration is lazy, we won't fire off an HTTP request until we actually ask for <code>data.c.f</code> from our data structure.</p>
<p>Because the hydrating data is used like a normal data structure, so we can pass the results from <code>hydrate()</code> to other functions and they will work without modification. We have injected lazy-loading data into our program!</p>
<h3>How does it work?</h3>
<p>This solution is based around the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy">Proxy</a> object, which comes with the Javascript standard library. (It is available in all browsers aside from Internet Explorer.) When we ask for a property of a proxy, eg <code>data.a</code>, we get to run code to determine what that property is. In the <code>get</code> function, on line 4, we check if we've already hydrated this object. If we have, we return the results of that so we don't end up repeating work and duplicating references. If we don't have the cached value, then on line 5 we check if we can construct a new one. This works by taking the constructor from the structure object, and the data from the data object, and calling the constructor with it. We store the results, which I've also seen called <em>memoization</em>, for the next time we're asked for this data. This avoids constructing the object again, which would be slow and cause problems - data.a might not equal data.a if we didn't store the reference, for example! Finally, if we don't have a constructor to call to validate the data and produce a hydrated object, we throw an error.</p>
<h3>Limitations</h3>
<p>This is demonstration code. It doesn't really handle lists, although that could be added in as another <code>if</code> statement. If you wanted to allow setting values in the hydrated data, you would have to add a <code>set</code>ter. The syntax for hydrating a sub-object could be improved by having the function return itself partially bound if only one arg is passed in. But these are all very approachable, and I think show the beauty of Javascript - a clean, functional style enabling a traditional, organized object-oriented approach.</p>
<p>There are, of course, libraries which will transport your objects better than this code, such that you don't have to maintain a mapping in the client-side code. (The first arg in the example function call.) However, they will be by necessity more heavy-weight and harder to understand. I am, personally, a programmer who likes to stay close to the browser. If you understand your tools and work with their strengths, you can make some amazingly performant, understandable web pages.</p>
<p class=noindent>No need to get complicated.</p>
<span class="tags">tags: <a href="/blog-posts/tags#js">js</a>, <a href="/blog-posts/tags#microblog">microblog</a>, <a href="/blog-posts/tags#web-dev">web dev</a></span>
<hr>
<h2 id="fixing-linux-audio-ubuntu-20-21-in-spring-2021">
<a href="/blog-posts/14.Fixing_Linux_Audio_Ubuntu_20_21_In_Spring_2021">Fixing Linux Audio, Spring '21 edition</a>
</h2>
<p>When upgrading from Ubuntu 20 to 21 this spring, my father and I both had the rear audio line out on our computers stop working. As we both had the same problem, I figured I would write it up in case anyone else was affected.</p>
<h3>Symptoms</h3>
<ol>
<li>The original audio device no longer shows up in the list of audio devices.</li>
<li>The device is not turned off or muted, and cannot be turned on or unmuted, because it doesn't exist any more.</li>
<li>Other devices (USB headphones, Bluetooth, etc.) still work.</li>
<li>When no other audio device is attached, a dummy audio device is created.</li>
<li>The troublesome audio device shows up when running some commands, I think such as <code>pacmd list-sinks</code>.</li>
</ol>
<h3>Confirmation</h3>
<ol>
<li>Running <code>sudo lsof /dev/snd/*</code> in a console shows Timidity holding open some files.</li>
<li>Ending the <code>timidity</code> process makes the audio device show up again.</li>
</ol>
<h3>Fix</h3>
<ol>
<li>Uninstall Timidity using your software manager, or by running <code>sudo apt remove timidity</code>.</li>
</ol>
<p class=noindent>Somewhat surprisingly, this does not seem to affect my ability to play back Midi files. I assume my music program is using Fluidsynth, which is a separate system? Nonetheless, a strange bug caused by a misconfiguration somewhere.</p>
<span class="tags">tags: <a href="/blog-posts/tags#linux">linux</a>, <a href="/blog-posts/tags#sound">sound</a>, <a href="/blog-posts/tags#audio">audio</a>, <a href="/blog-posts/tags#fix">fix</a>, <a href="/blog-posts/tags#tech-support">tech support</a>, <a href="/blog-posts/tags#workaround">workaround</a></span>
<hr>
<h2 id="shared-array-buffers-with-wasm">
<a href="/blog-posts/13.Shared_Array_Buffers_With_WASM">Shared Array Buffers With WASM</a>
</h2>
<p>Yesterday, I solved a long-standing question I'd had - how do you get data out of a WebAssembly program without having to copy it back? Ideally, in such a way that a web worker wouldn't have to copy it back to the main thread either. I've been able to find some information on this around the web, but much of it seems to be rather outdated or does not address the issue. I decided to have a crack at it myself and figure out the state of the art by writing a small proof-of-concept.</p>
<h3>Version 1</h3>
<p>My first approach was to try to create the web worker using a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer">SharedArrayBuffer</a> backing its code. As a bonus, we should be able to redefine bytecode on the fly then which will be fun.</p>
<p>Copying from <a href="https://depth-first.com/articles/2020/01/13/first-steps-in-webassembly-hello-world/">Depth-First's excellent guide</a> (read it before this post), we arrive at something like this:</p>
<code class="prism-block language-js">
<span class="token punctuation">(</span><span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> memory <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">WebAssembly<span class="token punctuation">.</span>Memory</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">initial</span><span class="token operator">:</span> <span class="token number">1</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> <span class="token function-variable function">log</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">offset<span class="token punctuation">,</span> length</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> bytes <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Uint8Array</span><span class="token punctuation">(</span>memory<span class="token punctuation">.</span>buffer<span class="token punctuation">,</span> offset<span class="token punctuation">,</span> length<span class="token punctuation">)</span>
<span class="token keyword">const</span> string <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">TextDecoder</span><span class="token punctuation">(</span><span class="token string">'utf8'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">decode</span><span class="token punctuation">(</span>bytes<span class="token punctuation">)</span><span class="token punctuation">;</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>string<span class="token punctuation">)</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token comment">//Blob generated with compile with `wat2wasm hello.1.wat --enable-threads --out /dev/stdout | base64 --wrap 0`</span>
<span class="token keyword">const</span> unsharedData <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">TextEncoder</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">encode</span><span class="token punctuation">(</span><span class="token function">atob</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">
AGFzbQEAAAABCQJgAn9/AGAAAAIZAgNlbnYGbWVtb3J5AgABA2VudgNsb2cAAAMCAQEHCQE
FaGVsbG8AAQoKAQgAQQBBDRAACwsTAQBBAAsNSGVsbG8sIFdvcmxkIQ==
</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token keyword">const</span> sharedData <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Uint8Array</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">SharedArrayBuffer</span><span class="token punctuation">(</span>unsharedData<span class="token punctuation">.</span>length<span class="token punctuation">)</span><span class="token punctuation">)</span>
sharedData<span class="token punctuation">.</span><span class="token function">set</span><span class="token punctuation">(</span>unsharedData<span class="token punctuation">)</span>
sharedData<span class="token punctuation">[</span>sharedData<span class="token punctuation">.</span>length <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token string">'?'</span><span class="token punctuation">.</span><span class="token function">charCodeAt</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">const</span> <span class="token punctuation">{</span> instance <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">await</span> WebAssembly<span class="token punctuation">.</span><span class="token function">instantiate</span><span class="token punctuation">(</span>sharedData<span class="token punctuation">,</span> <span class="token punctuation">{</span>
<span class="token literal-property property">env</span><span class="token operator">:</span> <span class="token punctuation">{</span> log<span class="token punctuation">,</span> memory <span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
instance<span class="token punctuation">.</span>exports<span class="token punctuation">.</span><span class="token function">hello</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
sharedData<span class="token punctuation">[</span>sharedData<span class="token punctuation">.</span>length <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token string">'.'</span><span class="token punctuation">.</span><span class="token function">charCodeAt</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
instance<span class="token punctuation">.</span>exports<span class="token punctuation">.</span><span class="token function">hello</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
</code>
<p>Here, we start by defining some <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Memory/Memory">WebAssembly memory</a> to pass args around with. (The <code>initial</code> value is in number of 64-KiB pages to allocate.) We then define a function, <code>log</code>, which will take this memory and print the contents using <code>console.log(…)</code>. We'll call this from our WASM code, which we've serialised in this case as a base64 string. (The source of which is <a href="/blog-posts/13.Shared_Array_Buffers_With_WASM/hello.1.wat">hello.1.wat</a>, compiled using <a href="https://webassembly.github.io/wabt/doc/wat2wasm.1.html">wat2wasm</a> from <a href="https://github.com/WebAssembly/wabt">WABT</a>.)</p>
<p>To get our shared memory, we create a new array backed by a <code>SharedArrayBuffer</code>. In JS, all the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray"><em>typed</em> arrays</a> have a backing buffer. Usually, by default, it's an <code>ArrayBuffer</code>. Amusingly enough, an <code>ArrayBuffer</code> <em>can</em> be shared between multiple typed arrays, even of different types. The <code>SharedArrayBuffer</code> is called so because it can be passed between web workers without copying as well, which a regular <code>ArrayBuffer</code> can't do. This is the behaviour we're after.</p>
<p>So, let's test it! First, we'll set the final byte of our WASM program to <code>?</code>, from it's original value of <code>!</code>, to prove we're loading the right memory and can manipulate it. Then, we start the WebAssembly program and call the <code>hello()</code> function of the <code>instance</code> we created. This in turn calls our <code>log()</code>, which prints "Hello, world?".</p>
<p class="noindent"><em>(Note: <code>WebAssembly.instantiate(…)</code> will also let you pass in an <code>ArrayBuffer</code>/<code>TypedArrayBuffer</code>, in addition to the <code>Uint8Array</code> we have here… in Firefox and not in Chrome.)</em></p>
<p>Now we modify our memory again, this time changing the final byte to <code>.</code>. However, calling into <code>hello</code> again, we find we get the same output, "Hello, world?". We can't just poke the memory of a running WASM program, it would seem - probably for the best. So, what do we do now?</p>
<h3>Version 2</h3>
<p>We have one other memory-buffer-ish object we can tweak. Let's see if we can't get that initial <code>const memory = …</code> declaration to be a shared buffer, instead of an unshared buffer. Some brief searching later, and we find that <code>WebAssembly.Memory</code> can indeed take a <code>shared</code> flag. It's not very well supported, but let's rework our code to try to test it anyway. (I believe the <code>shared</code> flag is part of the <a href="https://developers.google.com/web/updates/2018/10/wasm-threads">WebAssembly Threads</a> system, which seems to just be referring to using shared memory to communicate between workers VS message passing.)</p>
<code class="prism-block language-js">
<span class="token punctuation">(</span><span class="token keyword">async</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> memory <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">WebAssembly<span class="token punctuation">.</span>Memory</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">initial</span><span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token literal-property property">maximum</span><span class="token operator">:</span> <span class="token number">1</span><span class="token punctuation">,</span> <span class="token literal-property property">shared</span><span class="token operator">:</span><span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">let</span> globalBytes <span class="token operator">=</span> <span class="token keyword">null</span>
<span class="token keyword">const</span> <span class="token function-variable function">log</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token parameter">offset<span class="token punctuation">,</span> length</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> bytes <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Uint8Array</span><span class="token punctuation">(</span>memory<span class="token punctuation">.</span>buffer<span class="token punctuation">,</span> offset<span class="token punctuation">,</span> length<span class="token punctuation">)</span>
globalBytes <span class="token operator">=</span> bytes
<span class="token comment">//Can't use TextDecoder because it doesn't handle shared array buffers as of 2021-04-20.</span>
<span class="token comment">//const string = new TextDecoder('utf8').decode(bytes);</span>
<span class="token keyword">const</span> string <span class="token operator">=</span> bytes<span class="token punctuation">.</span><span class="token function">reduce</span><span class="token punctuation">(</span>
<span class="token punctuation">(</span><span class="token parameter">accum<span class="token punctuation">,</span>byte</span><span class="token punctuation">)</span><span class="token operator">=></span>accum<span class="token operator">+</span>String<span class="token punctuation">.</span><span class="token function">fromCharCode</span><span class="token punctuation">(</span>byte<span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token string">''</span><span class="token punctuation">)</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>string<span class="token punctuation">)</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
<span class="token comment">//Blob generated with compile with `wat2wasm hello.2.wat --enable-threads --out /dev/stdout | base64 --wrap 0`</span>
<span class="token keyword">const</span> wasm <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">TextEncoder</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">encode</span><span class="token punctuation">(</span><span class="token function">atob</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">
AGFzbQEAAAABCQJgAn9/AGAAAAIaAgNlbnYGbWVtb3J5AgMBAQNlbnYDbG9nAAADAgEBBwk
BBWhlbGxvAAEKCgEIAEEAQQ0QAAsLEwEAQQALDUhlbGxvLCBXb3JsZCE=
</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token keyword">const</span> <span class="token punctuation">{</span> instance <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token keyword">await</span> WebAssembly<span class="token punctuation">.</span><span class="token function">instantiate</span><span class="token punctuation">(</span>wasm<span class="token punctuation">,</span> <span class="token punctuation">{</span>
<span class="token literal-property property">env</span><span class="token operator">:</span> <span class="token punctuation">{</span> log<span class="token punctuation">,</span> memory <span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
instance<span class="token punctuation">.</span>exports<span class="token punctuation">.</span><span class="token function">hello</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
globalBytes<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token string">'\''</span><span class="token punctuation">.</span><span class="token function">charCodeAt</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
instance<span class="token punctuation">.</span>exports<span class="token punctuation">.</span><span class="token function">hello</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
</code>
<p>With our new memory declaration returning a shared buffer… on most non-Apple desktop browsers… 😬 we can now test this method of memory manipulation. We immediately find three things:
<ol>
<li>Our WASM program needs to have it's memory declaration updated too, yielding <a href="/blog-posts/13.Shared_Array_Buffers_With_WASM/hello.2.wat">hello.2.wat</a> and a new base64 blob.</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder"><code>TextDecoder</code></a> doesn't accept a SharedArrayBuffer, so we have to write our own little routine here. I guess this is because, as the bytes could change at any time, we could potentially output invalid utf-8 as our data shifted under us. We don't care for this single-threaded demo, but it would be an issue normally.</li>
<li>We must capture the newly-shared text buffer in the callback (as <code>globalBytes</code>), so we won't bother manipulating it before we instantiate our WebAssembly program.</li>
</ol>
<p>To test this, we call <code>hello()</code> again, which sets <code>globalBytes</code> to our "Hello, world!" message. We now set the first character to an apostrophe, and call in to our function again to test if we were able to set data visible to WASM. It prints "'ello, world!", thus demonstrating we are! Since we're working with a <code>SharedArrayBuffer</code> here, we can share this reference across threads to get fast, efficient data transfer.</p>
<span class="tags">tags: <a href="/blog-posts/tags#wasm">wasm</a>, <a href="/blog-posts/tags#js">js</a>, <a href="/blog-posts/tags#web-dev">web dev</a></span>
<hr>
<h2 id="same-script-multiple-tags">
<a href="/blog-posts/12.Same_Script_Multiple_Tags">Same Script, Multiple Tags</a>
</h2>
<p>Today, I happened on a fun quirk of web-dev. What happens if you include the same script twice, say like this:</p>
<code class="prism-block language-html">
<span class="token doctype"><span class="token punctuation"><!</span><span class="token doctype-tag">DOCTYPE</span> <span class="token name">html</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>html</span> <span class="token attr-name">lang</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>en<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>head</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>test.js<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>module<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation"><</span>script</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>test.js<span class="token punctuation">"</span></span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>module<span class="token punctuation">"</span></span><span class="token punctuation">></span></span><span class="token script"></span><span class="token tag"><span class="token tag"><span class="token punctuation"></</span>script</span><span class="token punctuation">></span></span>
</code>
<p><code>test.js</code> only runs once, despite being included twice. It seems modules have a specific trait where they're only ever evaluated once, I believe specified in <a href="https://262.ecma-international.org/6.0/#sec-moduleevaluation">steps 4 and 5 of ECMAScript 6.0 section 15.2.1.16.5</a>.</p>
<p class=noindent>Nothing good ever comes of running scripts multiple times. I'm glad it's out.</p>
<p class=noindent>P.S.: Fun fact: You can still <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/open"><code>document.write(…)</code></a> in an ECMAScript module, which just feels… <em>wrong.</em> (At least the <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with">with</a> statement is history there, as modules are always in strict mode.)</p>
<span class="tags">tags: <a href="/blog-posts/tags#js">js</a>, <a href="/blog-posts/tags#quirk">quirk</a>, <a href="/blog-posts/tags#microblog">microblog</a>, <a href="/blog-posts/tags#web-dev">web dev</a></span>
<hr>
<h2 id="graphics-card-test">
<a href="/blog-posts/11.Graphics_Card_Test">Graphics Card Exposure Test</a>
</h2>
<p>Sometimes, your graphics card's details are exposed to the web. <b id="graphics-card-test-output">Yours are not right now, though, which is good.</b> Try opening this page in different browsers – I found Chrome had much more information than Firefox, for example.</p>
<h3>The Code</h3>
<script>{
const getGPU = () => {
const ctx = document.createElement('canvas').getContext('webgl')
const ext = ctx.getExtension('WEBGL_debug_renderer_info')
return {
card: ctx.getParameter(ext.UNMASKED_RENDERER_WEBGL),
vendor: ctx.getParameter(ext.UNMASKED_VENDOR_WEBGL),
}
}
try {
const gpu = getGPU()
document.getElementById('graphics-card-test-output')
.textContent = `For example, you are running ${gpu.vendor}'s ${gpu.card}.`
} catch (e) {
console.info("Graphics card test failed.", e)
}
}</script>
<code class="prism-block language-js">
<span class="token comment">//Function to grab GPU data.</span>
<span class="token keyword">const</span> <span class="token function-variable function">getGPU</span> <span class="token operator">=</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> ctx <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">createElement</span><span class="token punctuation">(</span><span class="token string">'canvas'</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getContext</span><span class="token punctuation">(</span><span class="token string">'webgl'</span><span class="token punctuation">)</span>
<span class="token keyword">const</span> ext <span class="token operator">=</span> ctx<span class="token punctuation">.</span><span class="token function">getExtension</span><span class="token punctuation">(</span><span class="token string">'WEBGL_debug_renderer_info'</span><span class="token punctuation">)</span>
<span class="token keyword">return</span> <span class="token punctuation">{</span>
<span class="token literal-property property">card</span><span class="token operator">:</span> ctx<span class="token punctuation">.</span><span class="token function">getParameter</span><span class="token punctuation">(</span>ext<span class="token punctuation">.</span><span class="token constant">UNMASKED_RENDERER_WEBGL</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token literal-property property">vendor</span><span class="token operator">:</span> ctx<span class="token punctuation">.</span><span class="token function">getParameter</span><span class="token punctuation">(</span>ext<span class="token punctuation">.</span><span class="token constant">UNMASKED_VENDOR_WEBGL</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token comment">//Put the GPU data into the web page.</span>
<span class="token keyword">const</span> gpu <span class="token operator">=</span> <span class="token function">getGPU</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
document<span class="token punctuation">.</span><span class="token function">getElementById</span><span class="token punctuation">(</span><span class="token string">'graphics-card-test-output'</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span>textContent <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">Yours is </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>gpu<span class="token punctuation">.</span>vendor<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">'s </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>gpu<span class="token punctuation">.</span>card<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">.</span><span class="token template-punctuation string">`</span></span>
</code>
<span class="tags">tags: <a href="/blog-posts/tags#webgl">webgl</a>, <a href="/blog-posts/tags#privacy">privacy</a>, <a href="/blog-posts/tags#demo">demo</a>, <a href="/blog-posts/tags#web-dev">web dev</a></span>
<hr>
<h2 id="moving-domains">
<a href="/blog-posts/10.Moving_Domains">Moving Domains</a>
</h2>
<p>On Feb 23, 2012, I added a test page to my personal website at http://ddr0.github.com, using <a href="https://pages.github.com/">Github Pages</a>. Three years later, I purchased ddr0.ca and started redirecting the domain name to http://ddr0.github.io, which Github had migrated Pages to after <a href="https://github.blog/2013-04-05-new-github-pages-domain-github-io/">some security issues</a> arising from having them on a subdomain of the main site. A few years after that, I started hosting my website myself on a VPS at <a href="https://cloudatcost.com">Cloud at Cost</a>. I kept the Github Pages in case anything went wrong. Ultimately, it did - the VPS service slowly died of oversubscription a few months ago. (It was an excellent service for many years—and for less than a dollar a month!)</p>
<p>As my web server slowly fell out from under me, taking upwards of half a minute to run <code>ls</code>, I retreated to my living room where this site is hosted now. I had deployed a highly-accessible DnD-style dice roller, <a href="https://ddr0.ca/%F0%9F%8E%B2/">🎲</a>, but it required a <a href="https://nodejs.org/en/">Node</a>-based service running on the back-end to perform the rolls.</p>
<p>While I'd known it for some time, this really drove home that ddr0.ca is no longer hostable entirely on Github Pages. I need to maintain a server running it, so there's no reason to keep ddr0.github.io around. The <a href="https://github.com/DDR0/ddr0.github.com">code for ddr0.ca</a> will remain happily on Github itself, but starting today any requests to a web page on Github Pages will redirect to the equivalent page on ddr0.ca.</p>
<p>As I don't have a whole lot of control over how Github Pages serves my content, I've <a href="https://github.com/DDR0/ddr0.github.com/tree/github-pages-shell">replaced the content</a> of each HTML file with a <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta#examples">meta redirect</a> and deleted most of the other files. This means that, as far as I can manage, the URLs that have been linked around the web should still work. <a href="https://www.w3.org/Provider/Style/URI">Cool URLs don't change.</a></p>
<p>Moving forward, I should get the releases for my old projects (River Run, Cube Trains, and the Open Pixel Platformer demo) out of this repo. Github has the concept of releases now, and I would like to use that instead of the repo for my website. The files will still be hosted on this website as insurance against something going missing, of course, <em>since you only own something if it's on your computer.</em> And, of course, I'd like to update this blog a little more. I need a reason to implement pagination!</p>
<span class="tags">tags: <a href="/blog-posts/tags#meta">meta</a>, <a href="/blog-posts/tags#web-dev">web dev</a>, <a href="/blog-posts/tags#github">github</a></span>
<hr>
<h2 id="csp-on-html-assets">
<a href="/blog-posts/09.CSP_on_HTML_Assets">Web Security: Should CSP be set for my HTML files only, or my HTML files and all my assets?</a>
</h2>
<p>Yes. "Some web framework automatically generate html on error pages and we found xss issues in those in the past, so setting <a href="https://developer.mozilla.org/en/docs/Web/Security/CSP">CSP</a> on everything is best." --ulfr from <a href="irc://irc.mozilla.org">Moznet</a></p>
<span class="tags">tags: <a href="/blog-posts/tags#csp">csp</a>, <a href="/blog-posts/tags#web-dev">web dev</a>, <a href="/blog-posts/tags#xss">xss</a>, <a href="/blog-posts/tags#microblog">microblog</a></span>
<hr>
<h2 id="balancing-braces">
<a href="/blog-posts/08.Balancing_Braces">Balancing Braces</a>
</h2>
<p>I chanced to read an article this morning on <a href="https://medium.com/@benastontweet/balancing-braces-a76e7b8e5f04">Ben Aston's blog</a>, which dealt with balancing braces. You should go read it now, since it's good and since I'm about to critique the heck out of his approach.</p>
<p>The task Ben sets himself is simple: write a Javascript function, isBalanced, that returns true if a set of braces is balanced. Running his solution through <a href="https://jshint.com">jshint</a>, we find it has 13 statements with a <a href="https://en.wikipedia.org/wiki/Cyclomatic_complexity">cyclomatic</a> complexity score of 6. I think it is an inelegant solution for an elegant problem, as my solution came in at 4 statements with a complexity score of 2. (And mine doesn't repeat the characters to be matched either. <img class="emote" src="/images/wesnoth%20icons/icon_wink.gif"> )</p>
<h3>Analysis</h3>
<p>This can be looked at not as a parsing problem, but as a pattern-matching problem instead. You have to think about it recursively.</p>
<p>The key insight we will use is that <strong>this problem is as easy as repeatedly removing pairs of braces</strong>, such as <code>()</code> or <code>[]</code>. There is no case where the braces could be balanced without a pair of braces occurring in the string. By repeatedly removing all the paired braces, we will end up with either an empty string or the non-matching braces.</p>
<p>Let's work this through. <code>'[{}]'</code> has one pair of braces we can spot - <code>{}</code>. Remove it and we are left with <code>'[]'</code> and another pair, <code>[]</code>, which we can also remove. Are there any characters left? No? Then the input was balanced. However, <code>'[{]}'</code> has no pairs of braces, and does have characters left, so it's unbalanced. <code>'()['</code> reduces to <code>'['</code>, which is likewise unbalanced.</p>
<h3>Solution</h3>
<div class="code-container">
<code>function isBalanced(braces) {</code>
<code> do {</code>
<code> var matches = braces.split(/\(\)|{}|\[]/);</code>
<code> braces = matches.join('');</code>
<code> } while (matches.length > 1);</code>
<code> return !braces;</code>
<code>}</code>
</div>
<br>
<figure style="padding-top:1em">
<iframe src="blog-posts/08.Balancing_Braces/example" style="height:26ex" scrolling="no"></iframe>
<figcaption style="padding-top:1em"><strong>Live Example:</strong> All tests passing.</figcaption>
</figure>
<p>Given this solution, Ben's statement that "to solve this problem we need to have a function that will return the opener corresponding to a closer" is proven false. We have no such function, and we have solved the problem.</p>
<h3>Comparison</h3>
<p>In our code, <strong>there's a lot of things we don't have to worry about.</strong> We don't have to worry about inverting characters. We don't have to worry about queues. We don't have to worry about what the schema is, or even what <em>a</em> schema is. <strong>We just remove characters from a string.</strong></p>
<p>This post is a lot shorter than Ben's, because my solution is a lot simpler. Often you do not have to think of the right data structure, but instead spend some time thinking of the right algorithm. My solution is simpler, but perhaps Ben's is constrained by an outside influence, not stated in the problem - say, the user of the function will want an error message pointing to the mismatched brace. While we should always strive for the clearest code, it's worth remembering that sometimes it was done that way for a reason.</p>
<span class="tags">tags: <a href="/blog-posts/tags#code-golf">code golf</a>, <a href="/blog-posts/tags#response">response</a></span>
<hr>
<h2 id="wacom-for-hearthstone">
<a href="/blog-posts/07.Wacom_for_Hearthstone">Fixing Wacom Tablets for Hearthstone in Windows</a>
</h2>
<a id="WacomForHearthstone"></a> <!-- anchor to link to this post, from before we had individual post pages -->
<p>As of 2016-10-29, there is some sort of bug with Hearthstone where it will ignore clicks coming from a tablet. A quick search turns up complaints, but no solution:</p>
<ol>
<li><a href="https://us.battle.net/forums/en/hearthstone/topic/19136283676">Wacom tablet pen support has been removed with patch 3.1.010357</a></li>
<li><a href="https://eu.battle.net/forums/en/hearthstone/topic/16069796978">can't use mouse pen after update</a></li>
<li><a href="https://www.reddit.com/r/hearthstone/comments/4843fc/wacom_tablet_not_working_on_hearthstone/">Wacom Tablet not working on Hearthstone</a></li>
</ol>
<p><a href="https://ahkscript.org/">AutoHotKey</a> for Windows has no such issues. And it can send mouse inputs that hearthstone can read… <img class="emote" src="/images/wesnoth%20icons/icon_pensive.gif"> So, we'll make a new AutoHotKey script that clicks the left mouse button when the left mouse button is clicked. After installing AHK, make a new file with Notepad called wacom_echoer.ahk with the following contents:</p>
<!-- Code tag on each line because each line gets padded later in the compiling process too. -->
<div class="code-container">
<code>;Map the left mouse button to the left mouse button. This makes Hearthstone, among other</code>
<code>;games, "see" it.</code>
<code>#NoEnv ; Recommended for performance and compatibility with future AutoHotkey releases.</code>
<code>SendMode Input ; Recommended for new scripts due to its superior speed and reliability.</code>
<code>SetWorkingDir %A_ScriptDir% ; Ensures a consistent starting directory.</code>
<code>return</code>
<code></code>
<code>LButton::LButton</code>
</div>
<small>Edit: Fixed initial comments.</small>
<small>Edit: It also fixes Cities: Skylines!</small>
<p>Double-click to run. Hearthstone should now work as expected.</p>
<p>I also followed <a href="https://www.volnapc.com/all-posts/how-to-get-rid-of-those-annoying-circles-from-your-wacom-cursor-in-windows-7">these instructions to disable the click rings in Windows</a>.</p>
<span class="tags">tags: <a href="/blog-posts/tags#wacom">wacom</a>, <a href="/blog-posts/tags#tech-support">tech support</a>, <a href="/blog-posts/tags#hearthstone">hearthstone</a>, <a href="/blog-posts/tags#hack">hack</a>, <a href="/blog-posts/tags#workaround">workaround</a>, <a href="/blog-posts/tags#ahk">ahk</a></span>
<hr>
<h2 id="text-templating">
<a href="/blog-posts/06.Text_Templating">A Case Against Text Templating</a>
</h2>
<p>I recently had to deal with a mature PHP-based application. While quite well maintained, the code was suffering heavily from the effects of greedy serialization. Most templating languages, PHP included, work by going along and imperatively creating a long string of text. Echo this, buffer that, and concatenate the whole shebang into an outgoing HTML file. This is a powerful approach, beautiful in the simplicity of its method, and infinitely embeddable.</p>
<p>It is also a a trap.</p>
<p>Say you are creating a function to make a "breadcrumb trail" style nav bar. It's used on every page of your app, and it's pretty standard. You have a few ordered fields such as company, contact, and job; and a few custom ones which will be passed in as an argument. The function signature looks like getBreadcrumb(context[, extraListItems]).</p>
<p>Some time later, you find you need to give a custom ID to one of the standard breadcrumb links. So, you go back to the getBreadcrumb() function and add in another parameter, a list of IDs corresponding to the produced list of elements. Now the function looks like getBreadcrumb(context[, extraListItems[, listItemIDs]]).</p>
<p>After the ID mechanism is firmly entrenched in your codebase, you are asked to add a class to the second breadcrumb item. Because you're a good programmer, and can see where this is headed, you make the mechanism generic. You can now pass in a list of maps such as {id: 'foo', class: 'bar baz'}. Now, the signature looks like getBreadcrumb(context[, extraListItems[, listItemIDs[, listItemAttributes]]]).</p>
<p>A few months later, a bug report comes in. On one page in, say, the Financials module, the breadcrumb is showing up as being in the Reports module and people can't navigate back to their financials. When you look at it, you find it's a simple problem - it's a financial <em>report</em>. The fix adds in another parameter to the getBreadcrumbList function called 'hideAutomaticBreadcrumbs'.</p>
<p>Now we have a beast of a function with five arguments, one of which is redundant. (Many calls look like getBreadcrumb(this->context, null, null, [[], [], ['id'=>'currentJobName']]).) It's not even so much a big function as it is an <em>awkward</em> one. It's hard to test as well. What's more, the pattern has repeated itself across most of the code. Some of the functions even have more than a dozen arguments! How do we avoid this? At every step, adding one more arg seemed like the right thing to do.</p>
<p>The problem with the input of the function was actually caused by the output of the function. getBreadcrumb() returns strings of HTML, which are written to the document we're generating. However, this means that getBreadcrumb() is the <strong>final</strong> place we can manipulate and change the breadcrumbs. To fix this, let's try re-running our scenario. However this time, instead of returning HTML to be echo'd to the document, we will generate a tree structure to represent our HTML. Unlike the text representation, the tree will have all the relevant attributes open to manipulation later in the program. So, we might access the breadcrumb trail via html.body.topNav.breadcrumb. The second breadcrumb item might be accessed via something like breadcrumb.li[1], where breadcrumb is an ordered list.</p>
<p>Now that the structure is available to us, the custom breadcrumb id argument is thrown away – never existed – because we can simply go breadcrumb[1].li.id = "customID". We don't have to generate the list with customID, because we can change it after we've generated it. Same thing with the attributes list we added when we needed classes.</p>
<p>The bug report about one of the list items being incorrect is similarly solved by modifying the offending item in place, as a one-off change.</p>
<p>Now the getBreadcrumb(context[, extraListItems]) call is manageable. Special cases are handled locally, and the arguments list is not polluted with redundant behaviour. By discarding the structural information of our HTML document only after we're done creating all of it, we are free to manipulate it as we need to. Because we don't have to write a function to generate the perfect breadcrumb trail every time, we are able to write a simple one that just generates a good default breadcrumb trail.</p>
<span class="tags">tags: <a href="/blog-posts/tags#rant">rant</a>, <a href="/blog-posts/tags#html">html</a>, <a href="/blog-posts/tags#templating">templating</a>, <a href="/blog-posts/tags#php">php</a>, <a href="/blog-posts/tags#web-dev">web dev</a></span>
<hr>
<h2 id="calculating-a-bounce">
<a href="/blog-posts/05.Calculating_a_Bounce">Calculating a Bounce</a>
</h2>
<div class="no-p-indent">
<p>Problem: Given a ball and a curved wall, how do we calculate the angle of the bounce of the ball? Assuming we have the normal of the wall at the bounce location, our problem becomes:</p>
<p>Problem: Given two vectors, x₁ and n, how do we mirror vector x₁ around vector n to get x′? (x₁ is the ball velocity and n is the normal of the wall.)</p>
<p>Solution: Implement <a href="https://mathworld.wolfram.com/Reflection.html">https://mathworld.wolfram.com/Reflection.html</a> (The first picture is accurate to the situation.)</p>
<p>As written: x₁′ = -x₁ + 2x₀ + 2n̂[(x₁-x₀)·n̂]</p>
<p>Given that x₀ is always [0,0], it can be ignored.</p>
<p>x₁′ = -x₁ + 2n̂[x₁·n̂]</p>
<p>Given that n is pre-normalized, we can un-hat the ns.</p>
<p>x₁′ = -x₁ + 2n[x₁·n]</p>
<p>To calculate the dot product: (from <a href="https://www.mathsisfun.com/algebra/vectors-dot-product.html">https://www.mathsisfun.com/algebra/vectors-dot-product.html</a>)</p>
<p>x₁′ = -x₁ + 2n[x₁[0]*n[0]+x₁[1]*n[1]]</p>
<p>Normalize the notation, since we're now using [0] to get the vector components.</p>
<p>x₁′ = -x₁ + 2*n*(x₁[0]*n[0]+x₁[1]*n[1])</p>
<p>Now, to calculate both parts of the vector separately:</p>
<p>x₁[0]′ = -x₁[0] + 2*n[0]*(x₁[0]*n[0]+x₁[1]*n[1])</p>
<p>x₁[1]′ = -x₁[1] + 2*n[1]*(x₁[0]*n[0]+x₁[1]*n[1])</p>
<p>Now you can replace the x₁ and n with the variables of your program, and be on your way. For example, in Javascript:
<div class="code-container">
<code>//Returns vector v mirrored around the normalized vector mir.</code>
<code>function vReflectIn(v,mir) { </code>
<code> return [</code>
<code> -v[0] + 2*mir[0]*(v[0]*mir[0]+v[1]*mir[1]),</code>
<code> -v[1] + 2*mir[1]*(v[0]*mir[0]+v[1]*mir[1]),</code>
<code> ]; </code>
<code>}</code>
</div>
</div>
<span class="tags">tags: <a href="/blog-posts/tags#math">math</a>, <a href="/blog-posts/tags#bounce">bounce</a>, <a href="/blog-posts/tags#physics">physics</a></span>
<hr>
<h2 id="gifs-to-pngs">
<a href="/blog-posts/04.GIFs_To_PNGs">Batch Converting Gif Animations to Spritesheets with ImageMagick</a>
</h2>
<p>During some recent work on the <a href="http://www.pixeljoint.com/2015/01/21/4521/Open_Pixel_Platformer_is_back.htm">Open Pixel Platformer</a>, I had many .gif animations which I needed to make into spritesheets. To convert them all, I wrote a Bash script to automate the task.</p>
<!-- Code tag on each line because each line gets padded later in the compiling process too. -->
<div class="code-container">
<code>gifs=`find . -iname '*.gif'`</code>
<code>echo "Queuing $(echo "$gifs" | wc -l) gif animations to be converted to png spritesheets. Queued images may take a while to process in the background."</code>
<code>echo "$gifs" | while read gif; do</code>
<code> png=${gif/.gif/.png} #convert *.gif filename to *.png filename.</code>
<code> #echo queued "$gif"</code>
<code> </code>
<code> # Explanation of montage command:</code>
<code> # "$gif" \</code>
<code> # -tile x1 -geometry +0+0 \ #Set up the tiles.</code>
<code> # -border 1 -bordercolor \#F9303D -compose src -define 'compose:outside-overlay=false' \ #Draw a 1-px red border around the image, so it's easier to find frames. -compose is needed to make the border not fill in the transparent pixels in the image, and -define is needed to make the -compose not erease the previous gif frames we're compositing as we draw each subsequent one.</code>
<code> # -background "rgba(0, 0, 0, 0.0)" \ #Set the background to stay transparent, as opposed to white. (-alpha On seems to have no effect)</code>
<code> # -quality 100 \ #The default quality is 92, but since we're dealing with pixel art we want the fidelity.</code>
<code> # "$png" & #Run all the conversions in parallel, let the OS figure out scheduling. Replace with something smarter if things start lagging too much.</code>
<code> </code>
<code> montage \</code>
<code> "$gif" \</code>
<code> -tile x1 -geometry +0+0 \</code>
<code> -border 1 -bordercolor \#F9303D -compose src -define 'compose:outside-overlay=false' \</code>
<code> -background "rgba(0, 0, 0, 0.0)" \</code>
<code> -quality 100 \</code>
<code> "$png" &</code>
<code>done</code>
</div>
<span>View on <a href="https://github.com/DDR0/open_pixel_platformer/blob/d3b71ce7a5c8eea2fa43edeaef487bd870de1c29/images/google%20drive/animationToSpritesheet.zsh">Github</a></span><br>
<p>The script loops over any gifs found, and runs ImageMagick's <code>montage</code> on them to convert them to a png spritesheet. The output takes into account transparency of the original image and draws a border around each frame so you can easily find the right dimensions. To use the script, run it in the root folder containing everything you want to convert. The script should work in Bash on Mac or Linux if Imagemagick is installed, but it will not work on Windows.</p>
<span class="tags">tags: <a href="/blog-posts/tags#command-line">command line</a>, <a href="/blog-posts/tags#example">example</a>, <a href="/blog-posts/tags#imagemagick">imagemagick</a>, <a href="/blog-posts/tags#gif">gif</a>, <a href="/blog-posts/tags#png">png</a>, <a href="/blog-posts/tags#conversion">conversion</a></span>
<hr>
<h2 id="createjs-examples">
<a href="/blog-posts/03.CreateJS_Examples">Practical CreateJS Examples</a>
</h2>
<!-- This class is a trigger to start looking for elements with data-code-sources. The script pans through the entire page looking for it, because I don't know to to 'get elements after this element'. We break at the <hr> -->
<script class='start-code-viewer-scan'>
"use strict";
var createjsExamples = {
play: function play(iframe, url) {document.getElementById(iframe).src = url;},
stop: function stop(iframe) {document.getElementById(iframe).src = ""},
show: function show(iframe, url) {
var codeWindow = window.open("blog-posts/03.CreateJS_Examples/code viewer", '_blank');
codeWindow.blur();
function sendURL(event) {
if(event.data === "need-url") {
console.log('sending url');
codeWindow.postMessage({url:url, iframe:iframe}, window.location.origin);
codeWindow.removeEventListener("message", sendURL, false);
var listenForSelect = function(event) {
if(event.data && event.data.type) {
console.log('got event ' + event.data.type, event);
//if("event-type" === event.data.type) {}
}
};
//codeWindow.addEventListener("message", listenForSelect, false);
codeWindow.addEventListener("beforeunload", function() {
configureEvents(iframe, codeWindow, true);
});
configureEvents(iframe, codeWindow, false);
}
};
codeWindow.addEventListener("message", sendURL, false);