Repository navigation
Expand file tree
/
Copy pathmwWindow.c
More file actions
3549 lines (3079 loc) · 156 KB
/
Copy pathmwWindow.c
File metadata and controls
3549 lines (3079 loc) · 156 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 */
#include "mwLyapunovMath.h" /* IterateLyapunovExponent() - see SampleLyapunov() */
#include "mwNewtonMath.h" /* IterateNewton()/NewtonRootIndex() - see SampleNewton() */
#include "mwParameterDialog.h" /* ShowParameterDialog()/ParameterField - see ConfigureMultibrot() */
#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
/* Burning Ship's own default view - real -2.5..1.5, imaginary -1..2,
matching the full-fractal framing widely cited for it (e.g.
Wikimedia Commons' own "Burning Ship Fractal.png", lower-left
(-2.5,-1), upper-right (1.5,2)) rather than reusing Mandelbrot's
own (very different-shaped) default above. centreIm is positive
because this project's own pixel-to-plane mapping already has Im
increasing downward as pixel y increases (MapPixelToPlaneDouble()/
Fixed(), mwFractalMath.c) - the same convention several of the
sources above describe as giving the ship its traditional upright
orientation. Worth checking against the actual rendered image
regardless - if it comes out upside down, negating this value is
the entire fix. */
#define kBurningShipDefaultCentreRe -0.5
#define kBurningShipDefaultCentreIm 0.5
#define kBurningShipDefaultHalfWidthRe 2.0
/* Tricorn's own default view - the same box widely cited for both it
and plain Mandelbrot's classic (not this project's own tuned)
framing: real -2.5..1, imaginary -1..1 (e.g. HandWiki's Tricorn and
Burning Ship articles both use this exact box in their reference
pseudocode). Not reusing kMandelbrotDefaultCentreRe/Im/HalfWidthRe
above - those were tuned for the plain Mandelbrot shape specifically,
not verified to frame Tricorn's own, differently-proportioned
three-cusped shape well. */
#define kTricornDefaultCentreRe -0.75
#define kTricornDefaultCentreIm 0.0
#define kTricornDefaultHalfWidthRe 1.75
/* Multibrot's own default view - a generic, conservative Mandelbrot-
like framing rather than this project's own tuned one, since it
hasn't been verified (unlike Burning Ship/Tricorn above, sourced
from cited reference images) to frame every power 3-5 well; picked
to be safely unlikely to show an empty view for any of them, not
tuned for any one. */
#define kMultibrotDefaultCentreRe -0.5
#define kMultibrotDefaultCentreIm 0.0
#define kMultibrotDefaultHalfWidthRe 1.5
/* Phoenix's own default view - a generic, safe Mandelbrot-scale
framing, the same reasoning as Multibrot's own above: not sourced
from any specific cited rendering of this fractal (unlike Burning
Ship/Tricorn), just picked to be unlikely to show an empty view. Its
iteration keeps the same real-axis mirror symmetry plain Mandelbrot
has (p is real), so centreIm=0 is still the sensible choice here. */
#define kPhoenixDefaultCentreRe -0.5
#define kPhoenixDefaultCentreIm 0.0
#define kPhoenixDefaultHalfWidthRe 1.5
/* Lyapunov's own default view - not a complex plane at all (see
mwLyapunovMath.h's own comment), but the same gView/pixel-mapping
machinery reused for its a-b parameter plane instead: centreRe/Im
here are a and b's own centres, halfWidthRe their own half-range.
[2.5, 4.0] on both axes is the standard "interesting" region for the
driven logistic map - below about 2.5 either parameter just
converges to a stable fixed point with nothing structurally
interesting to see, and 4.0 is the map's own upper bound (x leaves
[0,1] above it). This is a real, well-established range for this
fractal specifically, not a generic placeholder the way Multibrot's/
Phoenix's own defaults above are. */
#define kLyapunovDefaultCentreRe 3.25
#define kLyapunovDefaultCentreIm 3.25
#define kLyapunovDefaultHalfWidthRe 0.75
/* Newton's own default view - the polynomial's own roots (the power-th
roots of unity) all sit exactly on the unit circle regardless of
power, so a view comfortably larger than that circle (real -2..2,
scaled to the window's own aspect for the imaginary axis) shows every
basin's own structure for any power this fractal offers - a real,
geometry-derived choice, not a generic placeholder the way
Multibrot's/Phoenix's own defaults are. kNewtonMaxIterations: Newton's
method converges quadratically, so most points settle in well under
10 iterations - 32 gives real headroom without the coarse-pass
iteration-ceiling reduction (UpdateIterationCeilingForBlockSize())
needing to matter much either way for this fractal. */
#define kNewtonDefaultCentreRe 0.0
#define kNewtonDefaultCentreIm 0.0
#define kNewtonDefaultHalfWidthRe 2.0
#define kNewtonMaxIterations 32
#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 - see kFractalTypes[]
below for which ID is which - and, before the person has ever
picked one, kNoFractalSelectedWidth (mwWindow.h), a sentinel meaning
"nothing selected yet". 0 rather than one past the last real type
(which is what this used to be, back when there were only ever
three types to be "one past"): a fixed offset like that collides
the moment a type gets added at that same ID, which is exactly what
very nearly happened when Burning Ship arrived - 0 is guaranteed
distinct from every real type's ID regardless of how many exist,
since real IDs start at 1 and DescriptorForWidth() has nothing to
look up for it, so RenderFractalOffscreen() and friends all
correctly do 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. */
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 };
/* Forward declaration only - see the full definition and kFractalTypes[]
itself further down this file (after the sample functions each row's
sampleProc field points to are forward-declared). A typedef to an
incomplete struct is fine to use as an opaque pointer, which is all
every caller before that point needs - but NOT to dereference a
member through, which needs the full definition visible at the
point of the dereference, not just at the point of the call. That
distinction is exactly what caught ResetViewForCurrentFractal()/
MaximumHalfWidthReForCurrentFractal()/
IsCurrentViewTheDefaultForCurrentFractal() out - all three actually
read a descriptor's own fields, not just pass the pointer around, so
all three had to move below the real struct definition instead of
living up here where it would have been more natural to group them
with ResetViewForCurrentFractal()'s own public declaration. Real
testing (an actual compile) is what caught this - none of the checks
this project could run without a working toolchain (brace/paren
balance, comment pairing) can catch a type-completeness error, since
it's a property of the language's own rules, not the text's shape. */
typedef struct FractalTypeDescriptor FractalTypeDescriptor;
static const FractalTypeDescriptor *DescriptorForWidth(short widthValue);
static double MaximumHalfWidthReForCurrentFractal(void);
static Boolean IsCurrentViewTheDefaultForCurrentFractal(void);
/* 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);
}
/* 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. MaximumHalfWidthReForCurrentFractal()
itself is defined later in this file (after kFractalTypes[]'s own
full definition, which it needs to dereference) - its forward
declaration above is enough for this call. */
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;
/* 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(). A fractal configure proc shows whatever
ShowParameterDialog() (mwParameterDialog.h) call a type needs before
it can render at all, returning false if the person cancelled -
NULL for every type that doesn't need one. A fractal direct-draw
proc draws a type that doesn't sample at all (the Tree; eventually
Fern/Sierpinski) - zero arguments deliberately, so this table can
dispatch through one function pointer type regardless of what
parameters any one type's own drawing function actually needs
internally (DrawBranch()'s x/y/angle/depth aren't meaningful for an
IFS fractal at all) - see DrawTreeOffscreen()/DrawTreeDirectly(). */
typedef short (*FractalSampleProc)(short x, short y);
typedef Boolean (*FractalConfigureProc)(void);
typedef void (*FractalDirectDrawProc)(void);
/* Forward declarations for kFractalTypes[] below, which references
these by name for its sampleProc/configureProc columns before any
of them are actually defined further down this file - a plain
identifier used this way (not as a call) needs a prior declaration
to be valid C at all, not just a style preference the way forward-
declaring an ordinary called function often is. */
static short SampleMandelbrot(short x, short y);
static short SampleJulia(short x, short y);
static short SampleBurningShip(short x, short y);
static short SampleTricorn(short x, short y);
static short SampleMultibrotConfigurable(short x, short y);
static short SamplePhoenix(short x, short y);
static short SampleLyapunov(short x, short y);
static Boolean ConfigureLyapunov(void);
static short SampleNewton(short x, short y);
static Boolean ConfigureNewton(void);
static Boolean ConfigureMultibrot(void);
static void DrawTreeOffscreen(void);
static void DrawTreeDirectly(void);
static void DrawFernOffscreen(void);
static void DrawFernDirectly(void);
static void DrawSierpinskiOffscreen(void);
static void DrawSierpinskiDirectly(void);
/* One row per fractal type - name, family (menu grouping - see
mwMenus.c's SetUpMenus() - and FractalFamilyForWidth()), which
function actually samples it (NULL for a direct-draw type - the
Tree, and eventually Fern/Sierpinski - which uses directDrawProc/
directDrawDirectProc instead, see RenderFractalOffscreen()/
DrawFractalDirectly()), its own iteration ceiling, its own default
view, its fixed constant if it has one (Julia only, so far - see
FractalTypeHasFixedConstant()), its own configuration step if it
needs one (Multibrot's power, via ConfigureMultibrot() - see
FractalTypeNeedsConfigurationAtIndex()/ConfigureFractalTypeIfNeeded()),
and its own pair of direct-draw functions if it doesn't sample at
all (the Tree's DrawTreeOffscreen()/DrawTreeDirectly(), thin
wrappers around DrawBranch()/DrawBranchDirectly() so this table can
dispatch through a plain zero-argument function pointer regardless
of what parameters any one type's own drawing function actually
needs internally). This replaces what used to be five separate
width==1/2/3 chains (FractalTypeNameForWidth(), FindFractalTypeByName(),
ResetViewForCurrentFractal(), MaximumHalfWidthReForCurrentFractal(),
RenderFractalOffscreen()/DrawFractalDirectly()'s own dispatch, and
GetFractalParameters()) - each one a place a new fractal type could
be added to some but not all of, silently. One table now, read by
DescriptorForWidth() below; adding a type is one new row.
typeID values are stable identifiers, not menu positions - nothing
here assumes typeID N sits at Fractal-menu item N (see
mwMenus.c's own comment on why that assumption broke). The values
themselves don't need to mean anything beyond "distinct" - existing
saved .frct files already carry the type by name (see
FindFractalTypeByName()), not by this number, so renumbering later
costs nothing. */
/* FractalSymmetryKind - whether, and how, a type's escape/convergence
result for a point is provably identical to its result for some
OTHER point derivable from it, given the CURRENT view happens to
sample both - see SampleWithSymmetryFold()'s own, much longer
comment for the mathematics, the exact view condition each kind
needs, and why this is fundamentally different from (and safer
than) the Mariani-Silver attempts documented above: those were
heuristics that could be wrong; this is an algebraic identity,
proven by induction for every type it's set on below, not assumed.
kFractalSymmetryNone: no exploitable symmetry, or none proven -
Burning Ship included deliberately (its own abs() operations break
the conjugate relationship every other Mandelbrot-shaped type here
has - confirmed against multiple independent sources, not just
derived), and Lyapunov/Fern/Sierpinski/Tree, none of which this
comment's own reasoning applies to at all.
kFractalSymmetryRealAxis: c and conj(c) give identical results -
Mandelbrot, Tricorn, Multibrot, Phoenix, and Newton all qualify (see
SampleWithSymmetryFold()'s own comment for the per-type proof
sketch), whenever the current view's own centreIm is exactly 0.0. */
typedef enum {
kFractalSymmetryNone,
kFractalSymmetryRealAxis
} FractalSymmetryKind;
struct FractalTypeDescriptor {
short typeID;
const char *name;
FractalFamily family;
FractalSampleProc sampleProc;
short maxIterations;
double defaultCentreRe;
double defaultCentreIm;
double defaultHalfWidthRe;
Boolean hasFixedConstant;
double constantRe;
double constantIm;
FractalConfigureProc configureProc;
FractalDirectDrawProc directDrawProc;
FractalDirectDrawProc directDrawDirectProc;
FractalSymmetryKind symmetryKind;
};
static const FractalTypeDescriptor kFractalTypes[] = {
{ 1, "Tree", kFractalFamilyRecursive, NULL, 0, 0.0, 0.0, 0.0, false, 0.0, 0.0, NULL, DrawTreeOffscreen, DrawTreeDirectly, kFractalSymmetryNone },
{ 10, "Barnsley Fern", kFractalFamilyRecursive, NULL, 0, 0.0, 0.0, 0.0, false, 0.0, 0.0, NULL, DrawFernOffscreen, DrawFernDirectly, kFractalSymmetryNone },
{ 11, "Sierpinski", kFractalFamilyRecursive, NULL, 0, 0.0, 0.0, 0.0, false, 0.0, 0.0, NULL, DrawSierpinskiOffscreen, DrawSierpinskiDirectly, kFractalSymmetryNone },
{ 2, "Mandelbrot", kFractalFamilyEscapeTime, SampleMandelbrot, kMandelbrotMaxIterations, kMandelbrotDefaultCentreRe, kMandelbrotDefaultCentreIm, kMandelbrotDefaultHalfWidthRe, false, 0.0, 0.0, NULL, NULL, NULL, kFractalSymmetryRealAxis },
{ 3, "Julia", kFractalFamilyEscapeTime, SampleJulia, kJuliaMaxIterations, kJuliaDefaultCentreRe, kJuliaDefaultCentreIm, kJuliaDefaultHalfWidthRe, true, kJuliaConstantRe, kJuliaConstantIm, NULL, NULL, NULL, kFractalSymmetryNone },
{ 4, "Burning Ship", kFractalFamilyEscapeTime, SampleBurningShip, kMandelbrotMaxIterations, kBurningShipDefaultCentreRe, kBurningShipDefaultCentreIm, kBurningShipDefaultHalfWidthRe, false, 0.0, 0.0, NULL, NULL, NULL, kFractalSymmetryNone },
{ 5, "Tricorn", kFractalFamilyEscapeTime, SampleTricorn, kMandelbrotMaxIterations, kTricornDefaultCentreRe, kTricornDefaultCentreIm, kTricornDefaultHalfWidthRe, false, 0.0, 0.0, NULL, NULL, NULL, kFractalSymmetryRealAxis },
{ 6, "Multibrot", kFractalFamilyEscapeTime, SampleMultibrotConfigurable, kMandelbrotMaxIterations, kMultibrotDefaultCentreRe, kMultibrotDefaultCentreIm, kMultibrotDefaultHalfWidthRe, false, 0.0, 0.0, ConfigureMultibrot, NULL, NULL, kFractalSymmetryRealAxis },
{ 7, "Phoenix", kFractalFamilyEscapeTime, SamplePhoenix, kMandelbrotMaxIterations, kPhoenixDefaultCentreRe, kPhoenixDefaultCentreIm, kPhoenixDefaultHalfWidthRe, false, 0.0, 0.0, NULL, NULL, NULL, kFractalSymmetryRealAxis },
{ 8, "Lyapunov", kFractalFamilyStatistical, SampleLyapunov, 0, kLyapunovDefaultCentreRe, kLyapunovDefaultCentreIm, kLyapunovDefaultHalfWidthRe, false, 0.0, 0.0, ConfigureLyapunov, NULL, NULL, kFractalSymmetryNone },
{ 9, "Newton", kFractalFamilyConvergence, SampleNewton, kNewtonMaxIterations, kNewtonDefaultCentreRe, kNewtonDefaultCentreIm, kNewtonDefaultHalfWidthRe, false, 0.0, 0.0, ConfigureNewton, NULL, NULL, kFractalSymmetryRealAxis }
};
#define kFractalTypeCount (sizeof(kFractalTypes) / sizeof(kFractalTypes[0]))
/* DescriptorForWidth()
The one row matching widthValue, or NULL for kNoFractalSelectedWidth
or anything else this build doesn't have - every caller below
already checks for NULL rather than assuming a match, the same
caution FractalTypeNameForWidth()'s own comment already called for
before this table existed. */
static const FractalTypeDescriptor *DescriptorForWidth(short widthValue) {
short i;
for (i = 0; i < (short) kFractalTypeCount; i++) {
if (kFractalTypes[i].typeID == widthValue)
return &kFractalTypes[i];
}
return NULL;
}
/* ResetViewForCurrentFractal()
See mwWindow.h. Does nothing for the Tree (no descriptor row - it
has no view at all) or an unrecognised width, exactly as the old
width==2/3 chain this replaced did for anything other than
Mandelbrot or Julia. Defined here, after kFractalTypes[]'s own full
definition above, rather than up near gView where it would read
more naturally next to its own declaration in mwWindow.h - it
dereferences a descriptor's own fields, which needs the complete
struct visible at the point of the dereference itself, not just a
forward-declared pointer to it (real testing - an actual compile -
caught this out; see the forward-declaration comment further up
this file for the full story). */
void ResetViewForCurrentFractal(void) {
const FractalTypeDescriptor *descriptor = DescriptorForWidth(width);
if (descriptor == NULL || !FractalTypeHasView(width))
return;
gView.centreRe = descriptor->defaultCentreRe;
gView.centreIm = descriptor->defaultCentreIm;
gView.halfWidthRe = descriptor->defaultHalfWidthRe;
}
/* 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 the Tree or an unrecognised 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.
Defined here rather than next to ClampHalfWidthRe() itself, for the
same struct-completeness reason as ResetViewForCurrentFractal()
above. */
static double MaximumHalfWidthReForCurrentFractal(void) {
const FractalTypeDescriptor *descriptor = DescriptorForWidth(width);
if (descriptor != NULL && FractalTypeHasView(width))
return descriptor->defaultHalfWidthRe;
return kMandelbrotDefaultHalfWidthRe;
}
/* 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 from this
same table, which is safe here because 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. False for the Tree (no descriptor row) or
an unrecognised width, same as ResetViewForCurrentFractal(). Defined
here rather than next to CacheOffscreenAsDefaultViewIfApplicable()
itself, for the same struct-completeness reason as
ResetViewForCurrentFractal() above. */
static Boolean IsCurrentViewTheDefaultForCurrentFractal(void) {
const FractalTypeDescriptor *descriptor = DescriptorForWidth(width);
if (descriptor == NULL || !FractalTypeHasView(width))
return false;
return gView.centreRe == descriptor->defaultCentreRe
&& gView.centreIm == descriptor->defaultCentreIm
&& gView.halfWidthRe == descriptor->defaultHalfWidthRe;
}
/* FractalTypeCount()/FractalTypeIDAtIndex()/FractalTypeNameAtIndex()/
FractalTypeFamilyAtIndex()
See mwWindow.h. Index isn't bounds-checked - every caller is
mwMenus.c's SetUpMenus(), looping 0..FractalTypeCount()-1 itself. */
short FractalTypeCount(void) {
return (short) kFractalTypeCount;
}
short FractalTypeIDAtIndex(short index) {
return kFractalTypes[index].typeID;
}
const char *FractalTypeNameAtIndex(short index) {
return kFractalTypes[index].name;
}
FractalFamily FractalTypeFamilyAtIndex(short index) {
return kFractalTypes[index].family;
}
Boolean FractalTypeNeedsConfigurationAtIndex(short index) {
return kFractalTypes[index].configureProc != NULL;
}
/* ConfigureFractalTypeIfNeeded()
See mwWindow.h. Looks widthValue up itself (rather than taking a
descriptor pointer) since HandleMenu() (mwMenus.c) - the only
caller - only ever has a type ID at this point, not a pointer into
a table it doesn't have access to. */
Boolean ConfigureFractalTypeIfNeeded(short widthValue) {
const FractalTypeDescriptor *descriptor = DescriptorForWidth(widthValue);
if (descriptor == NULL || descriptor->configureProc == NULL)
return true;
return descriptor->configureProc();
}
/* 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. Both are always prepared, not just whichever gHasFPU
would select: most sample functions only ever read the one that
matches gHasFPU, exactly as they dispatch on it for everything
else, but Lyapunov and Newton are always-double regardless of
gHasFPU (see IterateLyapunovExponent()'s own comment on why) and
need gRenderMappingDouble to be valid even on non-FPU hardware.
Preparing the one a given render won't actually use costs a few
divisions, once per render, not per pixel - negligible next to
anything else here. 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) {
PrepareFractalMappingDouble(&gRenderMappingDouble, &gView, windowWidth, windowHeight);
PrepareFractalMappingFixed(&gRenderMappingFixed, &gView, windowWidth, windowHeight);
}
/* SampleWithSymmetryFold() -------------------------------------------
The mathematics: for a type with kFractalSymmetryRealAxis
(mwWindow.h's own comment on the enum lists which), the escape or
convergence result for c is provably identical to the result for
conj(c) - proven here by induction for each type that carries the
flag, not assumed:
Mandelbrot/Multibrot (z -> z^n+c): if z(k) is c's own orbit, then
conj(z(k)) is conj(c)'s orbit, since conj(z^n+c) = conj(z)^n+conj(c)
for any integer n - conjugation commutes with both raising to a
power and addition. So |z(k)| = |conj(z(k))| at every step, and
the two orbits escape (or don't) at exactly the same iteration.
Tricorn (z -> conj(z)^2+c): a slightly different induction (see
mwFractalMath.c's own git history/commit reasoning if this is ever
revisited) shows conj(c)'s orbit is the conjugate of c's own, one
step delayed in how the conjugate gets reapplied - the escape time
still comes out identical either way.
Phoenix (z -> z^2+c+p*zPrev, p REAL): conjugating both z(k) and
zPrev(k) together is preserved by the update, precisely because p
has no imaginary part (conj(p*x) = p*conj(x) only when p is real) -
this project's own p=-0.5 (Ushiki's classic value) satisfies that.
Newton (z -> z - (z^n-1)/(n*z^(n-1))): z^n-1 has real coefficients,
so conj(f(z)) = f(conj(z)) for Newton's own update f - meaning
conj(z0) converges in exactly as many steps as z0 does, to
whichever root is conj(z0)'s own converged root's conjugate (not
necessarily the SAME root, unless it's the real one) - NOT
currently exploited here despite qualifying: SampleNewton() would
need to remap the cached root index to its own conjugate root, not
just reuse the cached shade level outright the way the other four
types can, and that remapping isn't implemented yet. Newton is
deliberately left off the symmetryKind list above until it is,
rather than marked eligible and produce a wrong shade for anyone
who actually zooms out enough to see two symmetric basins mixed up.
Burning Ship does NOT qualify - confirmed against multiple
independent published sources, not just derived here: its own
abs(Re)/abs(Im) step breaks the clean conjugate relationship the
moment either component is nonzero, which is essentially always.
This is categorically different from - and safer than - the
Mariani-Silver attempts DrawNextBlockAndAdvance()'s own comment
documents: those were heuristics (assume a block is uniform from a
handful of border samples) that could be, and were, wrong on real
testing. This is a proven algebraic identity: c and conj(c) don't
just *probably* match, they always do, for every type flagged above.
The only real risk here is a bookkeeping bug in the cache itself, not
the underlying maths being unsound - which is exactly why this is
verified against a plain reference (see the .c file this shipped
alongside) before being trusted.
The mechanism: whichever of a mirror pair (x,y)/(x, windowHeight-y)
is sampled FIRST in this pass computes normally and records its
shade level; the second one, whenever it's actually visited, finds
that value already cached and reuses it instead of sampling at all.
Quadrant order (MapIndexToQuadrantOrder()) doesn't process either
half of a pair in any guaranteed order, so the cache has to work
correctly regardless of which one arrives first - indexing by
min(y, windowHeight-y) (see gSymmetryCacheRowForY()) does that: both
members of a pair always land on the same cache row, whichever one
gets there first.
Applied ONLY at the finest, one-pixel-per-block pass
(fractalRenderJob.blockSize == 1, colour only - mono's own finest is
2x2, see CurrentFinestBlockSize(), and a 2x2 cell's own centre point
doesn't mirror as exactly onto another cell's centre the way a
single pixel does) and ONLY through the normal progressive path -
DrawFractalDirectly()'s low-memory fallback skips it entirely,
deliberately: that path exists for when memory is already tight, and
adding another allocation attempt there works against the exact
problem it exists to work around. Coarse passes aren't folded either -
already cheap relative to the finest pass (see kBlocksPerIdleSlice's
own comment on where render time actually goes), so the added
bookkeeping isn't worth it there.
y==0 is excluded from the fold: its own mirror, windowHeight, is one
past the last valid row, so there is no in-bounds partner to share
the work with - it always samples directly. This costs one row's
worth of pixels out of the whole image, not worth complicating the
indexing scheme to reclaim. */
static unsigned char *gSymmetryCache = NULL;
static short gSymmetryCacheRows = 0;
#define kSymmetryCacheEmpty 255 /* outside 0..kShadingScale, so it's unambiguous as "not yet computed" - see mwFractalMath.h for kShadingScale itself */
/* SymmetryFoldEligible()
True only when BOTH the current type's own symmetryKind and the
current view actually line up: a type flagged kFractalSymmetryRealAxis
only actually has mirror PAIRS to reuse when gView.centreIm is
exactly 0.0, which is what puts the sampled grid's own row windowHeight/2
exactly on the real axis (see MapPixelToPlaneDouble()'s own
derivation in mwFractalMath.c) - away from that, the view simply
doesn't sample any conjugate pairs at all, symmetric set or not. */
static Boolean SymmetryFoldEligible(void) {
const FractalTypeDescriptor *descriptor = DescriptorForWidth(width);
return descriptor != NULL
&& descriptor->symmetryKind == kFractalSymmetryRealAxis
&& gView.centreIm == 0.0;
}
/* AllocateSymmetryCache()/FreeSymmetryCache()
One byte per (x, canonical-row) slot, allocated fresh for whichever
render is about to start and freed the moment it ends or aborts -
see EndRendering(). Sized windowWidth * (windowHeight/2 + 1): every
pair's canonical row (min(y, windowHeight-y)) falls within
0..windowHeight/2 inclusive, so this covers every row that could
ever actually be looked up, with a little slack rather than an
exact-fit calculation that would need re-deriving carefully if the
indexing scheme here ever changes. A failed allocation just leaves
the fold disabled for this render (gSymmetryCache stays NULL,
SampleWithSymmetryFold() falls back to sampling directly every
time) - correct, just not optimised, exactly how this project
already treats AllocateOffscreenStore() failing.
FreeSymmetryCache() defined first, deliberately: AllocateSymmetryCache()
calls it defensively (see its own comment below), and a call before
the callee's own definition has been seen forces an implicit
declaration that then conflicts with the real static void
definition appearing later - real testing (an actual compile) is
what caught this, the same class of ordering mistake as
mwFractalMath.c's kFractalTypes[] forward-declaration issue earlier
in this project, just surfacing as a different diagnostic
("invalid redeclaration" here, "no such member" there) because a
missing function declaration and an incomplete struct type fail
differently, not because the underlying mistake is actually
different in kind. */
static void FreeSymmetryCache(void) {
if (gSymmetryCache != NULL) {
DisposePtr((Ptr) gSymmetryCache);
gSymmetryCache = NULL;
}
gSymmetryCacheRows = 0;
}
static void AllocateSymmetryCache(void) {
long size = (long) windowWidth * (windowHeight / 2 + 1);
/* Defensive: frees any cache already sitting in gSymmetryCache
before allocating a fresh one, rather than assuming
EndRendering() always ran first - costs nothing when it's
already NULL (FreeSymmetryCache()'s own no-op case), and avoids
a leak if that assumption is ever wrong. */
FreeSymmetryCache();
gSymmetryCache = (unsigned char *) NewPtr(size);
if (gSymmetryCache != NULL) {
gSymmetryCacheRows = windowHeight / 2 + 1;
/* memset(), not a Toolbox call: this is filling plain,
already-owned heap memory with a single repeated byte, the
textbook case for the ANSI library's own routine - this
project already links it (strcpy()/strcmp(), mwSaveAs.c and
elsewhere), so there's no new dependency being introduced. */
memset(gSymmetryCache, kSymmetryCacheEmpty, size);
}
}
/* SampleWithSymmetryFold() itself is defined further down, right after
fractalRenderJob's own declaration - it reads fractalRenderJob.sampleProc
directly, which isn't declared until then; real testing (another
actual compile) caught this exact same class of ordering mistake a
second time, this time against a plain struct variable rather than
a function or a type. */
/* 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;
/* SampleWithSymmetryFold()
See the "Real-axis mirror symmetry" section's own opening comment,
above SampleWithSymmetryFold()'s original home earlier in this file,
for the full mathematical picture - only the definition itself
moved down here, where fractalRenderJob (just above) is actually
visible; the reasoning above it didn't need to move with it. Called
from DrawNextBlockAndAdvance() in place of calling
fractalRenderJob.sampleProc() directly - falls straight through to
it, unchanged, whenever the fold doesn't apply (wrong type, wrong
view, cache never allocated, or y==0), so every render that doesn't
qualify behaves exactly as it did before this existed. */
static short SampleWithSymmetryFold(short x, short y) {
short mirrorY, canonicalRow;
long cacheIndex;
short shadeLevel;
if (gSymmetryCache == NULL || y == 0)
return fractalRenderJob.sampleProc(x, y);
mirrorY = windowHeight - y;
canonicalRow = (y < mirrorY) ? y : mirrorY;
if (canonicalRow >= gSymmetryCacheRows)
return fractalRenderJob.sampleProc(x, y); /* defensive only - shouldn't happen given AllocateSymmetryCache()'s own sizing, but a stale cache from a resize mid-render must never be read out of bounds */
/* (long) on canonicalRow forces the whole multiply into 32-bit
arithmetic before x is added - without it, canonicalRow*windowWidth
alone already overflows a 16-bit signed short well within this
project's own default 512x300 window (150*512 = 76800), not just
at some unusually large resize - the same reasoning
AllocateSymmetryCache()'s own size calculation already applies to
itself, missed here on the first pass through this function and
caught only by working the actual numbers, not by inspection. */
cacheIndex = x + (long) canonicalRow * windowWidth;
shadeLevel = gSymmetryCache[cacheIndex];
if (shadeLevel != kSymmetryCacheEmpty)
return shadeLevel;
shadeLevel = fractalRenderJob.sampleProc(x, y);
gSymmetryCache[cacheIndex] = (unsigned char) shadeLevel;
return shadeLevel;
}
/* 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;