-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmwWindow.c
More file actions
2348 lines (2049 loc) · 101 KB
/
Copy pathmwWindow.c
File metadata and controls
2348 lines (2049 loc) · 101 KB
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
/*****
* mwWindow.c
*
* The window routines for the Mandy Fractal Generator
*
*****/
#include <math.h>
#include <stdio.h>
#include <string.h>
#include <QDOffscreen.h>
#include "mwWindow.h"
#include "mwFractalMath.h" /* iteration, interior/shading maths - see that file */
#ifndef _Quickdraw_
#include <Quickdraw.h>
#endif
#ifndef _FixMath_
#include <FixMath.h> /* Fixed - Fixed-typed globals below still need this directly */
#endif
extern Boolean gHasColourQD; /* set once in MandyWindow.c's InitMacintosh() */
extern Boolean gHasFPU; /* set once in MandyWindow.c's InitMacintosh() */
#define windowX 0
#define windowY 40
#define pi 3.14159265
/* windowWidth/windowHeight - the content area's current size. These
were #define constants (512x300) before the window became
resizable (see HandleWindowResized()) - now runtime variables,
updated there and read everywhere else in this file exactly as
before, so a resize is visible everywhere that already reads them
by name (fractal coordinate mapping, buffer sizing via imageStart,
GetFractalResolution(), and so on) without those call sites needing
to change at all. windowX/windowY stay fixed constants - they're
only the window's initial on-screen position at launch, not
involved in resizing (the window's actual position afterward,
including after being dragged, is tracked by the Toolbox itself,
not by this project). */
static short windowWidth = 512;
static short windowHeight = 300;
/* Progressive-render tuning -------------------------------------------
kBlockGridTargetColumns: the coarsest pass aims for about this many
blocks across the longer side of the image (rounded down to a power
of two), which is what gives a 512-wide window 4 columns.
The finest pass size is NOT a fixed constant - see
CurrentFinestBlockSize() - because it differs between colour and
monochrome (colour refines all the way to real pixels; monochrome
stops one level short, at 2x2, to leave room for a dither pattern
simulating colour on a 1-bit screen).
kBlocksPerIdleSlice: how many blocks AdvanceFractalRender() draws
before yielding back to the event loop. Smaller keeps the app
checking for input more often (smoother, more responsive); larger
finishes a render sooner but leaves longer gaps between input
checks - though real timing suggests that trade-off matters less
than it looks: raising this from 4 to 16 alongside the adaptive
iteration ceiling and float precision changes below took real
render times from roughly 1000 seconds to roughly 250 - a large
enough combined win that per-tick overhead clearly wasn't the
limiting factor even at 16. Raised again to 32 on that basis, to
re-test the balance now that the underlying cost per tick has
dropped so much. Even at 32, a real render still yields many
thousands of times over its course, so Command-period
responsiveness shouldn't be noticeably affected - but this is a
real trade-off, not a free win, and worth watching if the render
ever feels unresponsive. */
#define kBlockGridTargetColumns 4
#define kBlocksPerIdleSlice 32
/* kBlitIntervalTicks: AdvanceFractalRender() used to call
BlitOffscreenToWindow() after every single kBlocksPerIdleSlice
batch - once per call, no exceptions. Real testing found an
optimisation that should clearly have helped (skipping iteration
entirely for the Mandelbrot set's main cardioid and period-2 bulb -
see IsInMainCardioidOrBulb()) produced no visible speed difference
at all. kBlocksPerIdleSlice's own comment above already shows fixed
per-tick overhead isn't the bottleneck (raising it from 4 to 16 was
a large part of an earlier ~4x win) - but CopyBits() itself scales
with the *area* it copies, not a fixed per-call cost, and
MapIndexToQuadrantOrder() scatters kBlocksPerIdleSlice blocks across
the image by design, so their bounding rect - what
BlitOffscreenToWindow() actually copies - can span most or all of
the image even when only a small fraction of it changed in that
batch. Throttling how often the blit actually happens, while still
sampling at full speed underneath, targets that directly: 6 ticks
(a tenth of a second) still looks smoothly progressive, but cuts
the number of CopyBits() calls roughly sixfold for a render that
would otherwise blit on every batch. AbortFractalRender()/a
finished render still force one final blit regardless, so nothing
ever finishes short of what it actually computed. */
#define kBlitIntervalTicks 6
/* kMinimumIterationCeiling: the floor UpdateIterationCeilingForBlockSize()
won't reduce a coarse pass's ceiling below - see that function for
why coarse passes get a reduced ceiling at all. kShadingScale, the
common range SampleMandelbrot()/SampleJulia() report shade levels
on regardless of which fractal's maxIterations produced them, now
lives in mwFractalMath.h alongside ShadeLevelForIterationCount(),
the function that actually produces values on that scale. */
#define kMinimumIterationCeiling 4
/* Mandelbrot and Julia's own natural default views - see FractalView
in mwWindow.h - expressed so that, at gView's default, rendering
matches this project's original fixed-zoom behaviour as closely as
possible.
Mandelbrot's matches exactly: the old code's zoom=150 meant
windowWidth/zoom pixels-per-unit, i.e. a visible Re width of
windowWidth/150 - halfWidthRe here is exactly half that, so the
default view covers the identical region.
Julia's Re range matches the old code's exactly (halfWidthRe=1.5
reproduces the old 1.5*(x-256)/256 term precisely), but its Im
range is very slightly different - about ±0.879 instead of the old
±1.0. The old code's Im scaling didn't actually follow the window's
real 512:300 aspect ratio (1.5 wide by 1.0 tall isn't 512:300) -
once halfWidthRe has to drive both axes consistently (so the
marquee zoom feature's pixel-to-plane mapping in mwZoom.c stays
correct at every zoom level, not just adds a special case for the
very first one), the default view has to follow that same aspect-
correct rule too, which shifts its vertical extent by about 12%. */
#define kMandelbrotDefaultCentreRe -0.293333
#define kMandelbrotDefaultCentreIm 0.0
#define kMandelbrotDefaultHalfWidthRe 1.706667
#define kJuliaDefaultCentreRe 0.0
#define kJuliaDefaultCentreIm 0.0
#define kJuliaDefaultHalfWidthRe 1.5
#define kMandelbrotMaxIterations 64
#define kJuliaConstantRe -0.7
#define kJuliaConstantIm 0.27015
#define kJuliaMaxIterations 300
/* Fixed-point equivalents of kJuliaConstantRe/Im, for
IterateEscapeTimeFixed() on the !gHasFPU path. Written as plain
integer literals rather than a DoubleToFixed(kJuliaConstantRe)-style
macro: that would textually re-expand to a floating-point multiply
at every use site, and while a good optimizer would constant-fold
two compile-time literals like that down to nothing, relying on
Think C actually doing so - rather than genuinely re-running it once
per pixel in SampleJulia(), reintroducing exactly the floating point
this path exists to avoid - isn't a chance worth taking for two
values that never change. -45875 and 17704 are -0.7 and 0.27015
each multiplied by 65536.0 and truncated toward zero, matching what
(Fixed) casting the double would produce; computed with a script
rather than by hand to keep the arithmetic itself trustworthy. */
#define kJuliaConstantReFixed ((Fixed) -45875)
#define kJuliaConstantImFixed ((Fixed) 17704)
/* Window title shown while idle, versus while a progressive render is
under way. "\021" is the Command-key glyph (Mac OS Roman code 0x11,
the same character AppendMenu()'s "/" syntax draws automatically in
menus) - it displays correctly in the title bar's system font on
any real Mac. Swap in a plain "Cmd-." if that glyph ever turns out
not to render as expected. */
#define kIdleWindowTitle "\pFractal Window"
#define kRenderingWindowTitle "\pFractal Window (\021. to abort)"
WindowPtr mwWindow;
Rect dragRect;
/* windowBounds/imageStart's initial values are written out literally
(matching windowWidth/windowHeight's own initial values above)
rather than computed from those variables, since C requires a
static initializer to be a compile-time constant - a plain variable
reference, even one that never actually changes before this line
runs, isn't allowed here. HandleWindowResized() updates both
directly, by assignment, on every actual resize. */
Rect windowBounds = { windowY, windowX, windowY+300, windowX+512 };
Rect imageStart = {0, 0, 300, 512};
/* width doubles as the fractal-type selector (1=Tree, 2=Mandelbrot,
3=Julia - see HandleMenu()'s fractalID case in mwMenus.c) and,
before the person has ever picked one, a sentinel meaning "nothing
selected yet" - deliberately a value none of the real fractal types
use, so RenderFractalOffscreen()'s width==1/2/3 checks all
correctly fall through to doing nothing, leaving the window blank
exactly as it is on a fresh launch. StartNewFractal() (mwMenus.c's
"New Fractal") resets back to this same value. */
#define kNoFractalSelectedWidth 5
int width = kNoFractalSelectedWidth;
/* The current Mandelbrot/Julia view - see FractalView in mwWindow.h.
Initialised to Mandelbrot's own default so it's never garbage even
before the very first ResetViewForCurrentFractal() call (which
always happens before either fractal is ever rendered - see
mwMenus.c - but this costs nothing to have anyway). */
FractalView gView = { kMandelbrotDefaultCentreRe, kMandelbrotDefaultCentreIm, kMandelbrotDefaultHalfWidthRe };
/* ResetViewForCurrentFractal()
See mwWindow.h. */
void ResetViewForCurrentFractal(void) {
if (width == 2) {
gView.centreRe = kMandelbrotDefaultCentreRe;
gView.centreIm = kMandelbrotDefaultCentreIm;
gView.halfWidthRe = kMandelbrotDefaultHalfWidthRe;
} else if (width == 3) {
gView.centreRe = kJuliaDefaultCentreRe;
gView.centreIm = kJuliaDefaultCentreIm;
gView.halfWidthRe = kJuliaDefaultHalfWidthRe;
}
}
/* MapPixelToComplexPlane()
See mwWindow.h. A convenience wrapper for mwFractalMath.h's own
Prepare/Map pair, for callers (mwZoom.c's marquee, mwSaveAs.c) that
only need an occasional one-off mapping and can afford to Prepare
fresh every call - unlike SampleMandelbrot()/SampleJulia()'s own
per-pixel hot path, which reuses gRenderMappingDouble/Fixed,
Prepared once per render (see PrepareRenderMapping()). Always
double, regardless of gHasFPU: the marquee only ever runs once per
drag, not once per pixel, so there's no case here for Fixed's speed
at the cost of its precision. */
void MapPixelToComplexPlane(short x, short y, double *outRe, double *outIm) {
FractalMappingDouble mapping;
PrepareFractalMappingDouble(&mapping, &gView, windowWidth, windowHeight);
MapPixelToPlaneDouble(&mapping, x, y, outRe, outIm);
}
/* MaximumHalfWidthReForCurrentFractal()
The current fractal's own default halfWidthRe - the ceiling
ClampHalfWidthRe() enforces, so zooming out repeatedly can't show an
ever-larger, eventually meaningless region beyond what the fractal
was ever meant to be viewed at. Falls back to Mandelbrot's own
default for any other width - shouldn't be reached in practice,
since callers check IsZoomAvailable() first, but returning a
sensible, real value here instead of leaving this undefined for a
caller that doesn't check first, does no harm. */
static double MaximumHalfWidthReForCurrentFractal(void) {
if (width == 3)
return kJuliaDefaultHalfWidthRe;
return kMandelbrotDefaultHalfWidthRe;
}
/* ClampHalfWidthRe()
See mwWindow.h. Picks between mwFractalMath.h's two precision
floors by gHasFPU - see their own comment there for why they
differ. This is the only place that distinction needs to be made:
every other caller (zoom in/out, marquee, FRCT load) reaches its
own halfWidthRe only through this function. */
double ClampHalfWidthRe(double proposedHalfWidthRe) {
double maximum = MaximumHalfWidthReForCurrentFractal();
double minimum = gHasFPU ? kFractalMinHalfWidthReDouble : kFractalMinHalfWidthReFixed;
if (proposedHalfWidthRe < minimum)
return minimum;
if (proposedHalfWidthRe > maximum)
return maximum;
return proposedHalfWidthRe;
}
/* Offscreen pixel store --------------------------------------------
The progressive renderer draws into this buffer; DrawContent() then
just copies finished pixels onto the screen. Two different
technologies back it depending on gHasColourQD:
- Monochrome: a plain BitMap with a manually allocated
baseAddr/rowBytes, wrapped in an ordinary GrafPort. This is the
classic pre-Color QuickDraw offscreen-bitmap technique, so it
works unmodified on real Mac Plus hardware.
- Colour: an 8-bit indexed GWorld with a small custom colour table
(see BuildFractalColourTable()) built to hold a smooth ramp across
kShadingScale. 8-bit indexed, rather than matching the screen's
actual depth, is deliberate: CopyBits() automatically dithers
this down to whatever the real screen supports (4-bit and up),
and an indexed image is what a future palette-cycling animation
(the "trippy" effect on the roadmap) needs to rewrite cheaply.
Only one of offscreenPort/offscreenBits or offscreenGWorld is ever
live at a time, selected by gHasColourQD; offscreenBounds and
offscreenReady describe whichever one is current. */
static GrafPort offscreenPort;
static BitMap offscreenBits;
static GWorldPtr offscreenGWorld;
static Rect offscreenBounds;
static Boolean offscreenReady = false;
/* Mono pattern-cycling support (see ApplyMonoPatternPhase()) --------
One byte per finest-size cell (CurrentFinestBlockSize() when not
rendering in colour), recording the shade level ShadeBlock() last
drew there - a coarse pass records the same level into every finest
cell under it, which a later, finer pass then overwrites with more
accurate values, so by the time a render completes every entry
reflects the actual final image, exactly like the pixels themselves.
Allocated unconditionally alongside the mono offscreen store itself
(see AllocateOffscreenMonoStore()) - most runs never turn animation
on and never read this, but it costs little to always have it ready
and already populated by the time they do. */
static unsigned char *gMonoShadeLevels = NULL;
static short gMonoShadeLevelColumns;
static short gMonoShadeLevelRows;
/* Default-view cache, for instant "Zoom Out" ------------------------
Caches the offscreen image - and, for mono, gMonoShadeLevels
alongside it, so Animate keeps working correctly on a restored
cache rather than redrawing from shade levels left over from
whatever zoomed view was rendered most recently - the moment a
render of the current fractal's own default view (see
ResetViewForCurrentFractal()) finishes naturally. See
CacheOffscreenAsDefaultViewIfApplicable(), called from
BeginNextPass() at exactly that point - not from an aborted render
(AbortFractalRender()), and not for the Tree, which doesn't use
gView at all.
One slot only, sized for whichever fractal is currently selected -
switching fractals overwrites it with a fresh cache for the newly
selected one the moment its own default view finishes rendering,
which happens immediately on every fractal switch (see mwMenus.c),
so there's never a need to cache more than one fractal's default at
once. gDefaultViewCacheWidth (matched against width, this file's
own global) records which fractal the cache is actually for, so a
restore attempt for the wrong one is refused rather than showing
the wrong image. */
static Ptr gDefaultViewCachePixels = NULL;
static long gDefaultViewCachePixelsSize = 0;
static unsigned char *gDefaultViewCacheShadeLevels = NULL;
static short gDefaultViewCacheWidth = 0;
/* IsCurrentViewTheDefaultForCurrentFractal()
True if gView currently holds exactly the current fractal's own
default view - an exact floating-point comparison against the same
literal constants ResetViewForCurrentFractal() assigns, which is
safe here for the same reason SampleMandelbrot()'s centreIm==0.0
check is: gView only ever holds one of these exact literals, or a
value computed by the marquee zoom feature's interpolation
(mwZoom.c), which would only match by the most remote coincidence. */
static Boolean IsCurrentViewTheDefaultForCurrentFractal(void) {
if (width == 2)
return gView.centreRe == kMandelbrotDefaultCentreRe
&& gView.centreIm == kMandelbrotDefaultCentreIm
&& gView.halfWidthRe == kMandelbrotDefaultHalfWidthRe;
if (width == 3)
return gView.centreRe == kJuliaDefaultCentreRe
&& gView.centreIm == kJuliaDefaultCentreIm
&& gView.halfWidthRe == kJuliaDefaultHalfWidthRe;
return false;
}
/* CacheOffscreenAsDefaultViewIfApplicable()
Snapshots the offscreen image (and, for mono, gMonoShadeLevels) into
the default-view cache, if the render that just finished was for
the current fractal's own default view - called only from
BeginNextPass()'s natural-completion branch, so an aborted render
never gets cached. A failed allocation just leaves the cache
invalid (gDefaultViewCacheWidth left not matching width) rather
than caching something partial - RestoreDefaultViewFromCache()
already falls back to a full render whenever the cache doesn't
apply, so there's nothing else to do here on failure. */
static void CacheOffscreenAsDefaultViewIfApplicable(void) {
BitMap *bits;
Rect bounds;
long pixelsSize;
if (!IsCurrentViewTheDefaultForCurrentFractal())
return;
if (!GetOffscreenImage(&bits, &bounds))
return;
pixelsSize = (long) bits->rowBytes * (bounds.bottom - bounds.top);
if (gDefaultViewCachePixels == NULL || gDefaultViewCachePixelsSize != pixelsSize) {
if (gDefaultViewCachePixels != NULL)
DisposePtr(gDefaultViewCachePixels);
gDefaultViewCachePixels = NewPtr(pixelsSize);
gDefaultViewCachePixelsSize = pixelsSize;
}
if (gDefaultViewCachePixels == NULL) {
gDefaultViewCacheWidth = 0;
return;
}
BlockMove(bits->baseAddr, gDefaultViewCachePixels, pixelsSize);
if (!gHasColourQD && gMonoShadeLevels != NULL) {
long shadeLevelsSize = (long) gMonoShadeLevelColumns * gMonoShadeLevelRows;
if (gDefaultViewCacheShadeLevels == NULL)
gDefaultViewCacheShadeLevels = (unsigned char *) NewPtr(shadeLevelsSize);
if (gDefaultViewCacheShadeLevels != NULL)
BlockMove(gMonoShadeLevels, gDefaultViewCacheShadeLevels, shadeLevelsSize);
}
gDefaultViewCacheWidth = width;
}
/* A fractal sample function reports how "escaped" the point at (x,y)
is, on the shared kShadingScale range - see SampleMandelbrot() and
SampleJulia(). */
typedef short (*FractalSampleProc)(short x, short y);
/* The iteration ceiling SampleMandelbrot()/SampleJulia() actually use
for whatever block is currently being sampled - see
UpdateIterationCeilingForBlockSize(). Explicitly set by every
caller of either sampler (RenderFractalOffscreen()'s pass
transitions, and DrawFractalDirectly()'s fallback path) rather
than derived implicitly from fractalRenderJob state, since
DrawFractalDirectly() runs with no progressive job - and hence no
meaningful fractalRenderJob.blockSize - at all. */
static short currentIterationCeiling;
/* gRenderMapping{Double,Fixed} - the pixel-to-plane mapping for
whichever fractal is currently rendering, precomputed once per
render (PrepareRenderMapping(), called from both
StartProgressiveRender() and DrawFractalDirectly()) rather than
re-derived per pixel - see mwFractalMath.h's own comment on why
this matters. Only the one gHasFPU-selected struct is ever
meaningful in a given render; SampleMandelbrot()/SampleJulia()
read whichever one applies, exactly as they already dispatch on
gHasFPU for everything else. Distinct from MapPixelToComplexPlane()'s
own, freshly-Prepared-per-call mapping (mwZoom.c/mwSaveAs.c's
occasional use) - these two never need to agree on freshness
since each caller Prepares its own. */
static FractalMappingDouble gRenderMappingDouble;
static FractalMappingFixed gRenderMappingFixed;
static void PrepareRenderMapping(void) {
if (gHasFPU)
PrepareFractalMappingDouble(&gRenderMappingDouble, &gView, windowWidth, windowHeight);
else
PrepareFractalMappingFixed(&gRenderMappingFixed, &gView, windowWidth, windowHeight);
}
/* Progressive render job -------------------------------------------
Tracks an in-progress coarse-to-fine render so AdvanceFractalRender()
can pick up where it left off each time it's called. There is only
ever one job at a time; starting a new one (RenderFractalOffscreen())
simply overwrites whatever was in progress.
Breadth-first across the whole image at every pass: every block at
the current size gets shaded before any of them subdivides further
- so the entire picture refines together, coming into focus as a
whole, rather than one region reaching full detail before the rest
are touched.
nextBlockIndex is a linear count (0 to columnCount*rowCount-1)
rather than a (column,row) pair - MapIndexToQuadrantOrder() turns it
into an actual grid position each time, in a recursively-quadrant-
grouped order rather than row-major. A plain row-major sweep looks
fine at coarse block counts (few enough blocks that a whole pass
finishes within one or two screen updates, so the order isn't
visible at all), but once a pass has enough blocks to take many
visible ticks, row-major becomes a visible left-to-right,
top-to-bottom scan - "line by line" - rather than looking like
quadrants filling in. long, not short: at the finest colour pass
this can run up to width*height (up to 153600 for this project's
512x300 image), which overflows a 16-bit short. */
static struct {
Boolean active;
FractalSampleProc sampleProc;
short blockSize;
short columnCount;
short rowCount;
long nextBlockIndex;
unsigned long startTick;
unsigned long endTick;
} fractalRenderJob;
/* Blit throttling state - see kBlitIntervalTicks' own comment.
Accumulates across possibly several AdvanceFractalRender() calls
until it's actually time to blit, rather than growing and shrinking
within a single call the way the job's own per-call changedRect
does. Reset (haveAccumulatedChanges cleared) whenever a render
starts - see BeginRendering() - since a fresh render's own initial
erase already invalidates any region a previous, now-superseded
render might have left pending. */
static Rect accumulatedChangedRect;
static Boolean haveAccumulatedChanges = false;
static unsigned long lastBlitTick = 0;
static void BeginRendering(void);
static void EndRendering(void);
static short SampleMandelbrot(short x, short y);
static short SampleJulia(short x, short y);
static RGBColor ColourForShadeLevel(short shadeLevel);
static unsigned short InterpolateComponent(unsigned short from, unsigned short to, double fraction);
static CTabHandle BuildFractalColourTable(void);
static short CurrentFinestBlockSize(void);
static Boolean ShouldRenderInColour(void);
static void ShadeBlock(const Rect *blockRect, short shadeLevel);
static void FillIndexedRect(const Rect *blockRect, short shadeLevel);
static short MonoBandIndexForShadeLevel(short shadeLevel, short phase);
static void FillMonoBand(const Rect *blockRect, short bandIndex);
static void RecordMonoShadeLevels(const Rect *blockRect, short shadeLevel);
static void DrawFractalDirectly(void);
static short BlocksAcross(short span, short blockSize);
static short HighestPowerOfTwoAtMost(short n);
static void MapIndexToQuadrantOrder(long index, short left, short top, short width, short height, short *outColumn, short *outRow);
static Boolean AllocateOffscreenMonoStore(void);
static Boolean AllocateOffscreenColourStore(void);
static Boolean AllocateOffscreenStore(void);
static void DisposeOffscreenStore(void);
static void StartProgressiveRender(FractalSampleProc sampleProc);
static void UpdateIterationCeilingForBlockSize(short blockSize);
static void DrawNextBlockAndAdvance(Rect *drawnRect);
static void AdvanceToNextBlock(void);
static void BeginNextPass(void);
static void EnterOffscreenPort(void);
static void EnterWindowPort(void);
static void BlitOffscreenToWindow(const Rect *changedRect);
static void DrawBranchDirectly(float x1, float y1, float angle, float depth);
static void DrawIndexedLine(short x1, short y1, short x2, short y2, short colourIndex);
static short BranchColourIndexForDepth(short depth);
static short RecursiveFractalBackgroundIndex(void);
/* SetUpWindow()
Create the Minimum Window window, and open it - a colour window via
NewCWindow() when Color QuickDraw is present, a plain monochrome one
via NewWindow() otherwise. Either way it's stored in the same
WindowPtr: CWindowRecord begins with a WindowRecord, so everything
elsewhere that reads windowKind/visible/portRect through mwWindow
(including all of mwMenus.c) works unchanged regardless of which
kind this actually is. */
void SetUpWindow(void) {
dragRect = screenBits.bounds;
if (gHasColourQD)
mwWindow = NewCWindow(0L, &windowBounds, kIdleWindowTitle, true, documentProc, (WindowPtr) -1L, true, 0);
else
mwWindow = NewWindow(0L, &windowBounds, kIdleWindowTitle, true, documentProc, (WindowPtr) -1L, true, 0);
SetPort(mwWindow);
RenderFractalOffscreen();
}
/* The Tree's fixed recursion depth, passed to the initial DrawBranch()/
DrawBranchDirectly() call at both call sites (RenderFractalOffscreen(),
DrawFractalDirectly()) - never actually varies, so it's a constant
rather than a parameter threaded through the recursion. Also drives
BranchColourIndexForDepth()'s base-to-tip colour mapping: depth
kTreeInitialDepth is the trunk (drawn first), depth 1 is the last
segment actually drawn before the depth-0 base case ends that
branch (the closest thing to a "tip" this recursion reaches). */
#define kTreeInitialDepth 9
/* DrawBranch()
Recursively draws the Tree's branches into the offscreen store for
the normal render path (RenderFractalOffscreen()) - the low-memory
DrawFractalDirectly() fallback, which has no offscreen store to
write into, uses DrawBranchDirectly() below instead.
In colour, writes each segment via DrawIndexedLine() - a plain
pixel-by-pixel write into the offscreen store's own memory - coloured
by BranchColourIndexForDepth(), rather than through LineTo()/
ForeColor(): QuickDraw's own colour-setting calls are exactly what
ShadeBlock()'s own comment (and FillIndexedRect(), which takes the
same direct-write approach for rects) already found unreliable on
this offscreen GWorld, for the same RGBForeColor()/PmForeColor()
reasons documented there. In monochrome, keeps the original
MoveTo()/Line() drawing unchanged - there's no palette to colour by
in monochrome, so there's nothing this change needs to do there. */
void DrawBranch(float x1, float y1, float angle, float depth) {
if (depth != 0) {
float x2 = x1 + cos(angle*(pi/180.0))*depth*10;
float y2 = y1 + sin(angle*(pi/180.0))*depth*10;
if (gHasColourQD) {
short colourIndex = BranchColourIndexForDepth((short) depth);
DrawIndexedLine((short) x1, (short) (windowHeight - y1), (short) x2, (short) (windowHeight - y2), colourIndex);
} else {
MoveTo(x1,windowHeight-y1);
Line(x2-x1,y1-y2);
}
DrawBranch(x2,y2,angle-20,depth-1);
DrawBranch(x2,y2,angle+20,depth-1);
}
}
/* DrawBranchDirectly()
The Tree's original drawing, unchanged: plain MoveTo()/Line() calls
into whatever the current port is. Used only by DrawFractalDirectly()'s
low-memory fallback, which draws straight into the window because
there's no offscreen store available to hold a palette-indexed
image at all - DrawBranch()'s direct-write approach above has
nothing to write into in that situation. */
static void DrawBranchDirectly(float x1, float y1, float angle, float depth) {
if (depth != 0) {
float x2 = x1 + cos(angle*(pi/180.0))*depth*10;
float y2 = y1 + sin(angle*(pi/180.0))*depth*10;
MoveTo(x1,windowHeight-y1);
Line(x2-x1,y1-y2);
DrawBranchDirectly(x2,y2,angle-20,depth-1);
DrawBranchDirectly(x2,y2,angle+20,depth-1);
}
}
/* SampleMandelbrot()/SampleJulia()
Map (x,y) through gRenderMappingDouble/Fixed (see PrepareRenderMapping()),
dispatching on gHasFPU exactly as the maths itself does - the
non-FPU path stays free of floating point from the pixel
coordinates onward, not just in the iteration loop. Mandelbrot
tests c = the mapped point (z starts at 0, and skips iteration
entirely when IsInMainCardioidOrBulb*() already proves it interior);
Julia iterates the mapped point as z against its own fixed c.
Both run against currentIterationCeiling - the cheaper, reduced
budget a coarse preview pass uses - but always shade against the
fractal's real maxIterations, not that reduced value: normalising
against whatever ceiling actually ran was tried first, and produced
visibly different colours pass to pass for the same point (log-
scaling the same count against a ceiling of 8 versus 64 gives very
different results); the true ceiling keeps colours stable as a
render refines. */
static short SampleMandelbrot(short x, short y) {
short iterationCount;
if (gHasFPU) {
double dRe, dIm;
MapPixelToPlaneDouble(&gRenderMappingDouble, x, y, &dRe, &dIm);
if (IsInMainCardioidOrBulb(dRe, dIm))
iterationCount = currentIterationCeiling;
else
iterationCount = IterateEscapeTimeDouble(0.0, 0.0, dRe, dIm, currentIterationCeiling);
} else {
Fixed fRe, fIm;
MapPixelToPlaneFixed(&gRenderMappingFixed, x, y, &fRe, &fIm);
if (IsInMainCardioidOrBulbFixed(fRe, fIm))
iterationCount = currentIterationCeiling;
else
iterationCount = IterateEscapeTimeFixed(0, 0, fRe, fIm, currentIterationCeiling);
}
return ShadeLevelForIterationCount(iterationCount, kMandelbrotMaxIterations);
}
static short SampleJulia(short x, short y) {
short iterationCount;
if (gHasFPU) {
double dRe, dIm;
MapPixelToPlaneDouble(&gRenderMappingDouble, x, y, &dRe, &dIm);
iterationCount = IterateEscapeTimeDouble(dRe, dIm, kJuliaConstantRe, kJuliaConstantIm, currentIterationCeiling);
} else {
Fixed fRe, fIm;
MapPixelToPlaneFixed(&gRenderMappingFixed, x, y, &fRe, &fIm);
iterationCount = IterateEscapeTimeFixed(fRe, fIm, kJuliaConstantReFixed, kJuliaConstantImFixed, currentIterationCeiling);
}
return ShadeLevelForIterationCount(iterationCount, kJuliaMaxIterations);
}
/* The colour ramp shadeLevel is mapped onto, in the same direction as
the monochrome buckets below: 0 (fast escape) is light or dark
depending on the palette's own aesthetic, kShadingScale (slow
escape, or never) is the palette's other extreme. Each palette is
its own list of colour stops, interpolated the same way regardless
of how many stops it has - a palette with 3 stops (Greyscale) and
one with 7 (Rainbow) are handled identically by ColourForShadeLevel().
Order in kPalettes[] must match the Palette submenu's AppendMenu()
string in mwMenus.c exactly - GetCurrentPalette()/SetCurrentPalette()
work in terms of this array's 0-based index, which the (1-based)
menu item number maps onto directly. */
typedef struct {
short shadeLevel;
RGBColor colour;
} ColourRampStop;
/* The offscreen colour table reserves two fixed entries beyond the
kShadingScale+1 palette-driven shading range: a genuine white and a
genuine black, used only as backgrounds for recursive/direct-draw
fractals (currently just the Tree - see RecursiveFractalBackgroundIndex()
and DrawBranch()). Neither is ever touched by a palette rebuild
(RebuildOffscreenColourTableForCurrentPalette() only ever writes
0..kShadingScale) or by Animate's colour-table rotation
(mwColourCycle.c's RotateColourTable() only ever rotates that same
range, via GetRotatableColourTableEntryCount()) - they stay a true
white and true black regardless of which palette is active or how
far it's been rotated, which matters for a palette like Night that
has no true white or black stop of its own to fall back on. */
#define kBackgroundWhiteIndex (kShadingScale + 1)
#define kBackgroundBlackIndex (kShadingScale + 2)
#define kColourTableEntryCount (kShadingScale + 3)
#define kMaxColourRampStops 7
typedef struct {
const char *name;
short stopCount;
ColourRampStop stops[kMaxColourRampStops];
} PaletteDefinition;
/* name is used two ways: mwMenus.c's Palette submenu string must list
these in this exact same order (AppendMenu() takes one hardcoded
Pascal string, not this array, so the two have to be kept in sync
by hand - see the comment there), and GetPaletteName()/
FindPaletteByName() below use it directly for saving/loading a
palette by name in a FRCT file (see mwSaveAs.c) rather than by this
array's index, so a saved file's meaning survives even if palettes
are ever reordered. */
static const PaletteDefinition kPalettes[] = {
/* Default - white through yellow/orange/red-purple to black; the
original ramp, unchanged from before palettes existed. */
{ "Default", 5, {
{ 0, { 65535, 65535, 65535 } },
{ kShadingScale / 4, { 65535, 65535, 0 } },
{ kShadingScale / 2, { 65535, 16384, 0 } },
{ (kShadingScale * 3) / 4, { 32768, 0, 16384 } },
{ kShadingScale, { 0, 0, 0 } }
}},
/* Night - dark navy through indigo and deep purple to near-black. */
{ "Night", 4, {
{ 0, { 0, 0, 16384 } },
{ kShadingScale / 3, { 8192, 0, 32768 } },
{ (kShadingScale * 2) / 3, { 24576, 0, 40960 } },
{ kShadingScale, { 4096, 0, 8192 } }
}},
/* Stormy - pale grey through slate grey and charcoal to near-black,
a cool undertone throughout. */
{ "Stormy", 4, {
{ 0, { 49152, 49152, 53248 } },
{ kShadingScale / 3, { 28672, 28672, 32768 } },
{ (kShadingScale * 2) / 3, { 12288, 12288, 16384 } },
{ kShadingScale, { 2048, 2048, 4096 } }
}},
/* Summery - white through bright yellow and sky blue to grass
green. */
{ "Summery", 4, {
{ 0, { 65535, 65535, 65535 } },
{ kShadingScale / 3, { 65535, 65535, 16384 } },
{ (kShadingScale * 2) / 3, { 16384, 49152, 65535 } },
{ kShadingScale, { 8192, 49152, 8192 } }
}},
/* Autumnal - pale gold through orange and rust red to deep brown. */
{ "Autumnal", 4, {
{ 0, { 65535, 57344, 32768 } },
{ kShadingScale / 3, { 65535, 32768, 8192 } },
{ (kShadingScale * 2) / 3, { 49152, 16384, 4096 } },
{ kShadingScale, { 24576, 8192, 4096 } }
}},
/* Wintery - white through pale ice blue and pale grey to soft
blue-grey. */
{ "Wintery", 4, {
{ 0, { 65535, 65535, 65535 } },
{ kShadingScale / 3, { 53248, 60416, 65535 } },
{ (kShadingScale * 2) / 3, { 45056, 45056, 49152 } },
{ kShadingScale, { 28672, 32768, 40960 } }
}},
/* Pastel - soft pink, lavender, mint, pale yellow, soft peach - all
high-lightness, low-saturation. */
{ "Pastel", 5, {
{ 0, { 65535, 53248, 57344 } },
{ kShadingScale / 4, { 53248, 49152, 65535 } },
{ kShadingScale / 2, { 49152, 65535, 57344 } },
{ (kShadingScale * 3) / 4, { 65535, 65535, 49152 } },
{ kShadingScale, { 65535, 57344, 49152 } }
}},
/* Rainbow - a full hue sweep: red, orange, yellow, green, blue,
indigo, violet. */
{ "Rainbow", 7, {
{ 0, { 65535, 0, 0 } },
{ (kShadingScale * 1) / 6, { 65535, 32768, 0 } },
{ (kShadingScale * 2) / 6, { 65535, 65535, 0 } },
{ (kShadingScale * 3) / 6, { 0, 65535, 0 } },
{ (kShadingScale * 4) / 6, { 0, 0, 65535 } },
{ (kShadingScale * 5) / 6, { 24576, 0, 65535 } },
{ kShadingScale, { 40960, 0, 65535 } }
}},
/* Fire (suggested) - white through bright yellow and orange to
deep red then black - hotter and more saturated than Default. */
{ "Fire", 5, {
{ 0, { 65535, 65535, 65535 } },
{ kShadingScale / 4, { 65535, 65535, 8192 } },
{ kShadingScale / 2, { 65535, 24576, 0 } },
{ (kShadingScale * 3) / 4, { 49152, 0, 0 } },
{ kShadingScale, { 0, 0, 0 } }
}},
/* Ocean (suggested) - white through cyan and teal to deep navy. */
{ "Ocean", 4, {
{ 0, { 65535, 65535, 65535 } },
{ kShadingScale / 3, { 16384, 57344, 65535 } },
{ (kShadingScale * 2) / 3, { 0, 32768, 40960 } },
{ kShadingScale, { 0, 4096, 16384 } }
}},
/* Greyscale (suggested) - pure white through mid grey to black, no
hue at all - a plain baseline, and cheap to reason about when
debugging shading itself independent of any palette's own
colour choices. */
{ "Greyscale", 3, {
{ 0, { 65535, 65535, 65535 } },
{ kShadingScale / 2, { 32768, 32768, 32768 } },
{ kShadingScale, { 0, 0, 0 } }
}}
};
#define kPaletteCount ((short) (sizeof(kPalettes) / sizeof(kPalettes[0])))
/* Persists across fractal switches (not reset by
ResetViewForCurrentFractal()) and across renders - a palette choice
is a display preference, not part of any one fractal's own state. */
static short currentPalette = 0;
/* InterpolateComponent()
Linear blend of one RGBColor component between two ramp stops. */
static unsigned short InterpolateComponent(unsigned short from, unsigned short to, double fraction) {
return (unsigned short) (from + (to - from) * fraction);
}
/* ColourForShadeLevel()
Finds the pair of the current palette's ramp stops shadeLevel falls
between and linearly blends their colours - identical logic
regardless of which palette is selected or how many stops it has. */
static RGBColor ColourForShadeLevel(short shadeLevel) {
const PaletteDefinition *palette = &kPalettes[currentPalette];
short i;
for (i = 1; i < palette->stopCount; i++) {
if (shadeLevel <= palette->stops[i].shadeLevel) {
short rangeStart = palette->stops[i-1].shadeLevel;
short rangeEnd = palette->stops[i].shadeLevel;
double fraction = (rangeEnd > rangeStart) ? (double) (shadeLevel - rangeStart) / (rangeEnd - rangeStart) : 0.0;
RGBColor result;
result.red = InterpolateComponent(palette->stops[i-1].colour.red, palette->stops[i].colour.red, fraction);
result.green = InterpolateComponent(palette->stops[i-1].colour.green, palette->stops[i].colour.green, fraction);
result.blue = InterpolateComponent(palette->stops[i-1].colour.blue, palette->stops[i].colour.blue, fraction);
return result;
}
}
return palette->stops[palette->stopCount - 1].colour;
}
/* IsCurrentPaletteDark()
Whether the active palette reads as predominantly dark overall -
averages a standard perceptual luma weighting (0.30/0.59/0.11,
scaled by 100 to stay in integer arithmetic) across all of the
palette's own stops, compared against the midpoint of the RGB
component range. Used to choose a contrasting background for
recursive fractals - see RecursiveFractalBackgroundIndex() - so a
palette like Night (all dark stops) gets a light background rather
than another dark one on top of it, and a palette like Wintery (all
pale stops) gets a dark one. */
static Boolean IsCurrentPaletteDark(void) {
const PaletteDefinition *palette = &kPalettes[currentPalette];
long total = 0;
short i;
for (i = 0; i < palette->stopCount; i++) {
RGBColor colour = palette->stops[i].colour;
total += ((long) colour.red * 30 + (long) colour.green * 59 + (long) colour.blue * 11) / 100;
}
return (total / palette->stopCount) < 32768;
}
/* RecursiveFractalBackgroundIndex()
The background colour index for recursive/direct-draw fractals -
currently just the Tree, via RenderFractalOffscreen(). Chosen for
contrast against whichever palette is active, rather than a fixed
colour: kBackgroundWhiteIndex if the palette reads as predominantly
dark overall (IsCurrentPaletteDark()), kBackgroundBlackIndex if it
reads as predominantly light. Both are the two fixed,
palette-independent entries described in kBackgroundWhiteIndex/
kBackgroundBlackIndex's own comment, so this is a genuine white or
black background regardless of what colours the active palette
itself happens to define. */
static short RecursiveFractalBackgroundIndex(void) {
return IsCurrentPaletteDark() ? kBackgroundWhiteIndex : kBackgroundBlackIndex;
}
/* BranchColourIndexForDepth()
Maps DrawBranch()'s current recursion depth onto a palette index
spanning the full shading range: kTreeInitialDepth (the trunk, drawn
first) to 0, and depth 1 (the last segment actually drawn before
the depth-0 base case ends a branch, the closest this recursion
gets to a "tip") to kShadingScale - so Animate's existing
colour-table rotation (mwColourCycle.c), completely unchanged,
already produces the requested "cycle from base to tip through the
palette" effect once branches carry these indices: rotating the
table shifts whichever colour was at the trunk toward the tips (or
the reverse, depending on rotation direction), with no
animation-specific code of its own needed here. */
static short BranchColourIndexForDepth(short depth) {
return (short) (((kTreeInitialDepth - depth) * kShadingScale) / (kTreeInitialDepth - 1));
}
/* BuildFractalColourTable()
Hand-builds a ColorTable of kColourTableEntryCount entries (a Handle
sized for ColorTable's trailing variable-length ctTable array): one
per possible shadeLevel (0..kShadingScale), so the offscreen
GWorld's CLUT is our own fractal ramp rather than the system
default, plus the two fixed white/black background entries - see
kBackgroundWhiteIndex/kBackgroundBlackIndex's own comment. The
caller owns the returned handle; NewGWorld() copies what it needs
from it rather than keeping it, so it should be disposed (via
DisposeCTable()) once passed to NewGWorld(). Returns NULL on low
memory. */
static CTabHandle BuildFractalColourTable(void) {
long tableSize = sizeof(ColorTable) + (long) (kColourTableEntryCount - 1) * sizeof(ColorSpec);
CTabHandle colourTable = (CTabHandle) NewHandle(tableSize);
short i;
if (colourTable == NULL)
return NULL;
(**colourTable).ctSeed = GetCTSeed();
(**colourTable).ctFlags = 0;
(**colourTable).ctSize = kColourTableEntryCount - 1;
for (i = 0; i <= kShadingScale; i++) {
(**colourTable).ctTable[i].value = i;
(**colourTable).ctTable[i].rgb = ColourForShadeLevel(i);
}
(**colourTable).ctTable[kBackgroundWhiteIndex].value = kBackgroundWhiteIndex;
(**colourTable).ctTable[kBackgroundWhiteIndex].rgb.red = 65535;
(**colourTable).ctTable[kBackgroundWhiteIndex].rgb.green = 65535;
(**colourTable).ctTable[kBackgroundWhiteIndex].rgb.blue = 65535;
(**colourTable).ctTable[kBackgroundBlackIndex].value = kBackgroundBlackIndex;
(**colourTable).ctTable[kBackgroundBlackIndex].rgb.red = 0;
(**colourTable).ctTable[kBackgroundBlackIndex].rgb.green = 0;
(**colourTable).ctTable[kBackgroundBlackIndex].rgb.blue = 0;
return colourTable;
}
/* CurrentScreenDepth()
The main screen's current pixel depth in bits (1, 2, 4, 8, 16, or
32). Checked fresh each time rather than cached, since it's cheap
(a couple of field reads, no searching) and it means a depth change
made mid-session via the Monitors control panel is picked up on the
next render rather than needing a relaunch. Assumes a single
display, matching the simplification already made elsewhere for
this app's fixed small window. */
short CurrentScreenDepth(void) {
GDHandle mainDevice = GetMainDevice();
PixMapHandle mainDevicePixMap = (**mainDevice).gdPMap;
return (**mainDevicePixMap).pixelSize;
}
/* ShouldRenderInColour()
Color QuickDraw being present isn't by itself a reason to draw in
colour: at 1-bit and 2-bit depths the render should look exactly
like a genuine black-and-white Mac, with no attempt at colour at
all, rather than colour that then gets dithered down to almost
nothing meaningful. This is the single place that decision is made;
ShadeBlock() and CurrentFinestBlockSize() both defer to it instead
of checking gHasColourQD directly. */
static Boolean ShouldRenderInColour(void) {
return gHasColourQD && (CurrentScreenDepth() >= 4);
}
/* CurrentFinestBlockSize()
Colour refines all the way to real 1x1 pixels. Monochrome - which
now includes 1-bit and 2-bit colour screens, not just genuinely
monochrome ones, see ShouldRenderInColour() - stops one level short,
at 2x2, leaving room for a dither pattern to simulate colour at the
finest visible unit - the same 2x2 granularity the original
hand-written Mandelbrot()/Julia() sampled at, now generalised to
every block size via ShadeBlock(). */
static short CurrentFinestBlockSize(void) {
return ShouldRenderInColour() ? 1 : 2;
}
/* ShadeBlock()
Colours a block according to how far up the shared kShadingScale its
sample fell. In monochrome, that's one of QuickDraw's standard
dither patterns via FillRect() - the mechanism is unchanged from
before this file supported colour, but the threshold values are
evenly-spaced fifths of kShadingScale rather than the original
uneven split. That split was tuned for a linear iteration-to-shade
mapping; once that became the log-scale mapping in
ShadeLevelForIterationCount(), it left "black" firing for almost
any non-trivial iteration count (roughly shadeLevel > 32 turns out
to correspond to a raw Mandelbrot iteration count of only about 7
out of 64) - washing out the characteristic grey detail band into a
solid black interior, a real regression caught on real testing.
Evenly dividing the range instead spreads the five bands back
across where log-scaled values actually land. This affects only
the monochrome branch; the colour ramp below was already
recalibrated for the log scale when that mapping was introduced.
In colour it's a direct pixel-memory write via FillIndexedRect()
rather than any QuickDraw colour-setting call: shadeLevel already
*is* the correct index into our own colour table
(BuildFractalColourTable() constructs it that way on purpose), so
there's nothing to search for or match - we already know the exact
byte we want written. This is the second colour-setting approach