The Boston Diaries

The ongoing saga of a programmer who doesn't live in Boston, nor does he even like Boston, but yet named his weblog/journal “The Boston Diaries.”

Go figure.

Monday, January 01, 2024

Free at last! Free at last! Thank God almighty the mouse is free at last!

Well, at least the 1928 version of Micky Mouse is free and in the public domain. It's no coincidence that Disney has worked the past few years turning the 1928 version of Micky into a trademark. Grab the popcorn! It's going to be an interesting year in copyright law.

Unlike the past few years, this year came in pretty quiet. Yes, there were some fireworks off in the distance, but our neighbors? Very quiet this year. Maybe that's a good sign that this year will be less explosive.

I can hope.

HAPPY NEW YEAR!

Monday, January 08, 2024

I wonder why today, of all days, I'm feeling this level of melancholy?

Yesterday at the “every-other-week” D&D game (played via Zoom these days), I couldn't bring up the virtual map the DM uses because my web browser didn't support WebGL, despite my updating the entire operating system, including the web browser, just 10 minutes earlier.

Sigh. What the XXXX happend to being just 20 minutes out of date?

Even worse, it ran the last time we played a month ago.

Admid suggestions from the rest of the group about what to do (even with the suggestion to not use the map at all and just go for “theater of the mind”), I just gave up and bailed out of the game. I just couldn't cope with the fact that despite my attempts at keeping up to date with all this technological XXXX being forced down our throats, it still wasn't XXXXX­XX good enough.

I think this is the culmination of my feelings toward the latest round of AI. I saw this video with Dr. Matt Welsh who joked that at his company, not using ChatGPT should be a firable offence, and I felt offended at the joke. Are we so XXXXX­XX beholden to our tools that we give up mastery of them? Of course, as the owner of a software company, he loves the idea of ChatGPT to write his software—it keeps his costs down. XXXX people who spent time investing in writing software.

As I commented a month ago:

I wrote about this about fifteen years ago, about the fear of becoming beholden to technology without understanding. IT currently doesn’t incentivize deep learning, or even mastery of anything, both because the perceived need to move fast, and the ever changing technology landscape. Who has time to learn anything any more? Why even bother if it’s all going to change anyway? Especially when we have a program that can do all that pesky learning for us?

Edit to add: Maybe this post describes it better?

LLMs make Programming Language Learning Curves Shallower | Lobsters

Why bother indeed?

And to bring this back around to D&DDeck of DM Things has made series of videos about using AI to run a game:

Even though he wasn't fully successful with the experient, as YouTuber Philip DeFranco is fond of saying, “this is the worst that AI will ever be.”

This has been in the back of my mind for some time now. I left The Enterprise partly because of the testing, and a fear I had was that not that AI would write the tests for us (which I wouldn't mind) but that I would have to write tests for the code AI wrote. It was bad enough having to endure endless computer updates and reboots when the XXXXX­XX computer mandated it—but to be truly enslaved to the machine?

Who controls who?

And it all came crashing through when I couldn't load a XXXXX­XX map on a XXXXX­XX web page.

I don't like the direction the industry is going, with all the constant changes just for its own sake (yes, I know, changes for security is a real thing, but that's not carte blanche to change how the rest of the software works). And often times, it's the computer in change. “Oh! Update me now!” “Oh! Reboot me now!” “Oh! Feed me now, Seymour!”

XXXX that XXXX!

Oh great! The network connection is down. Could be Roko's Basilisk giving me a warning, or just a momentary glitch. Who knows?

Sigh.

I feel like I'm yelling at the clouds to get off my lawn as I adjust my onion on my belt. But on a lighter note, ChatGPT in French is “cat, I farted.” That is truly wonderful.

Thursday, January 18, 2024

“Now, here, you see, it takes all the running you can do, to keep in the same place.”

I'm a bit relunctant to write this, as I'll come across as an old man yelling at the clouds to get off his lawn, but the whole “update treadmill” the Computer Industry has foisted on us is getting tiresome.

Bunny now wears a CGM prescribed by her doctor. It's a small disk that adhears to the back of the upper arm and sends readings of blood sugar via Bluetooth. Bunny has an app on her smartphone that records the information and forwards it to her doctor. The app is fine; no real problems with using it.

Until this morning.

Bunny woke me up to inform me that the app just stopped working, because her smartphone hadn't been updated in the previous 20 minutes.

What the XXXX

It was working fine the previous night. What updates were required? And why drop support for older operating systems? Oh yeah … right … it's hard to support systems older than 20 minutes.

And some PM somewhere needed to justify their job.

Sigh.

Things are working fine, how that the operating system on her smart phone was updated (only took several hours). But still … gah!


“… and water is wet! Film at 11!”

Time for more yelling at the clouds.

A few days ago I was surfing on my iPad when I came across An Astronomy Club of Brevard, NC. Now, my iPad is a bit, shall we say, slightly out of date? So it was with sadness when I saw:

Your Browser Is
No Longer Supported

To view this website and enjoy a better online experience,
update your browser for free.

Followed by a list of browsers.

You know, I thought we left that behind in the early 2000s, but apparently not.

I then viewed the site on my desktop computer and … why? Why do I need a less-than-20-minute old browser to view seven images and some text? Why does it take 193 requests to even show the page? At a minimum, you have the HTML, CSS and seven images, so … nine requests? Okay, maybe some Javascript to do the animations designers are so fond of.

But 193 files?

I'm not blaming the Astronomy Club of Brevard for this. They're using Wix to create and host the website, so I'm laying the blame solely at Wix here for going completely overboard with the JavaScript. Do web developers know you can create a perfectly good site with just HTML and CSS?

Yeah, I'm yelling at the tide to stop coming in.

Sigh.

Friday, January 19, 2024

Complicating code

I recently added an .OPT directive to my 6809 assembler. This allows me to add options to a source file instead of having to always specify them on the command line. I originally did this to support unit testing and I feel it's a nice addition.

When the test backend is enable, all the memory of the emulated 6809 is marked as non-readable, non-writable, non-executable. As the code is assembled, memory used by instructions are switched to “readable, executable” and memory used by data becomes “readable, writable” (easy, because of an early decision to have separate functions to write instructions vs. data). If you reference memory outside of the addresses used by the source code being assembled, you have to specify the permissions of said addresses.

; The set up for our tests.
; We should only read from these locations

	.opt	test prot r,ECB.beggrp,ECB.beggrp + 1
	.opt	test prot r,$112

; We can read and write to these locations

	.opt	test prot rw,$0E00,$0E00 + 1023

; Set the stack for our tests

	.opt	test stack $FF00

; Initialize some VM memory

	.opt	test memw,ECB.beggrp,$0E00
	.opt	test memb,$112,2

You really only need these for memory locations defined outside the source code. In the example above, the memory referenced is defined by the Color Computer and not by the code being tested, so they need to be initialized. And because the system only supports 65,536 bytes of memory, we can easily (on modern systems) assign permissions per byte.

So this all works and is great at finding bugs.

But then I thought—I can use .OPT to supress warnings as well. If I assemble the following code:

;**************************************************************************
;	frame_buffer		set frame buffer address	(GPL3+)
;Entry:	A - MSB of frame buffer
;Exit:	D - trashed
;**************************************************************************

frame_buffer	ldb	PIA0BC		; wait for vert. blank
		bpl	frame_buffer
.now		stx	,--s		; save X
		ldx	#SAM.F6		; point to framebuffer address bits
.setaddress	clrb			; reset B
		lsla			; get next address bit
		rolb			; isolate it
		stb	b,x		; and set the SAM F bit
		leax	-2,x		; point to next F bit register
		cmpx	#SAM.F0		; more?
		bhs	.setaddress	; more ...
		puls	x,pc		; return

I get

frame_buffer.asm:9: warning: W0002: symbol 'frame_buffer.now' defined but not used

The subroutine has effectively two entry points—frame_buffer will wait until the next vertical blanking interrupt before setting the address, while frame_buffer.now will set it immediately. The former is good if you are using a double-buffer system for graphics, while the later is fine for just switching the address once. Given that my assembler will warn on unused labels, this means I'll always get this error when including this code. I can supress that warning by issuing a -nW0002 on the command line, but then this will miss other unused labels that might indicate an actual issue.

I wanted to have something like this:

	.opt	* disable W0002
frame_buffer	...
.now		...

		puls	x,p
	.opt	* enable W0002

We first disable the warning for the code fragment, then afterwards we enable it again. I coded this all up, but it never worked. It worked for other warnings, but not this particular one.

The assembler is a classic two-pass assembler, but not all warnings are issued during the passes, as can be seen here (using a file that generates every possible warning with debug output enabled):

[spc]lucy:~/source/asm/a09/misc>../a09 -ftest -d -o/dev/null warn.asm
warn.asm: debug: Pass 1
warn.asm:2: warning: W0010: missing initial label
warn.asm:10: warning: W0008: ext/tfr mixed sized registers
warn.asm:11: warning: W0001: label 'a_really_long_label_that_exceeds_the_internal_limit_its_quite_l' exceeds 63 characters
warn.asm:16: warning: W0001: label 'a_really_long_label_that_exceeds_the_internal_limit_its_quite_l' exceeds 63 characters
warn.asm:21: warning: W0001: label 'another_long_label_that_is_good.but_this_makes_it_too_long_to_u' exceeds 63 characters
warn.asm:23: warning: W0001: label 'another_long_label_that_is_good.but_this_makes_it_too_long_to_u' exceeds 63 characters
warn.asm:36: warning: W0013: label 'a' could be mistaken for register in index
warn.asm:37: warning: W0013: label 'b' could be mistaken for register in index
warn.asm:38: warning: W0013: label 'd' could be mistaken for register in index
warn.asm: debug: Pass 2
warn.asm:2: warning: W0003: 16-bit value truncated to 5 bits
warn.asm:3: warning: W0004: 16-bit value truncated to 8 bits
warn.asm:4: warning: W0005: address could be 8-bits, maybe use '<'?
warn.asm:5: warning: W0006: offset could be 5-bits, maybe use '<<'?
warn.asm:8: warning: W0005: address could be 8-bits, maybe use '<'?
warn.asm:9: warning: W0007: offset could be 8-bits, maybe use '<'?
warn.asm:11: warning: W0009: offset could be 8-bits, maybe use short branch?
warn.asm:13: warning: W0011: 5-bit offset upped to 8 bits for indirect mode
warn.asm:25: warning: W0012: branch to next location, maybe remove?
warn.asm:26: warning: W0012: branch to next location, maybe remove?
warn.asm:43: warning: W0017: cannot assign the stack address within .TEST directive
warn.asm:42: debug: Running test test
warn.asm:42: warning: W0014: possible self-modifying code
warn.asm:42: warning: W0016: memory write of 00 to 0034
warn.asm:42: warning: W0015: : reading from non-readable memory: PC=0016 addr=F015
warn.asm:42: debug: Post assembly phases
warn.asm:2: warning: W0002: symbol '.start' defined but not used
[spc]lucy:~/source/asm/a09/misc>

You can see some are generated during pass 1, some during pass 2. The message “Running test test” happens after the second pass is done, and the one I'm trying to supress, W0002, at the very end of the program. The .OPT directives are processed during passes 1 and 2. There's just no easy way to supress W0002 just for a portion of the code, as I would have to carry forward that any labels defined between the disable and enable of W0002 should be exempt from the “no-label warning” check.

It's issues like these that complicate programs over time.

I was about to scrap the idea when I came up with a solution. Each symbol in the symbol table has a reference count. At the end of assembly, there's code that goes through the symbol table and issues the warning if a label has a reference count of 0. All I did was create another option, .OPT * USES <label>, to increment the reference count of a label. At the end of the day, it works. I'm not saying this is a “good” solution, just “a” solution.

Wednesday, January 24, 2024

Well, that was weird

Bunny's pharmacy just called to remind us to call and remind them to renew Bunny's prescription. Um … okay?


Adventures in high dining

Bunny and I found oursevles at La Nouvelle Maison, an upscale eating establishment in downtown Boca Raton. I'm not sure why she picked the place—perhaps because our previous upscale dining establishment, Root Italian Kitchen is no longer open. Or perhaps it's “just because.”

In any case, we had a reservation for 8:00 pm. I made it yesterday, as Bunny was running about Chez Boca worrying about the dress code (turns out—“business casual” so I don't have to don my International Man of Mystery clothes). I received a text notification a few hours later, and a phone call today, asking to confirm our time.

We arrived at 8:00 pm after a slight detour through the valet parking lot of Trattoria Romana next door (the driving directions from Mr. Google were slightly unclear on that). We walked right in, stated our reservation name and time, and were immedately seated in the quite crowded restaurant. I found that quite surprising, given how busy it all looked.

One thing about high dining that I don't think I'll get used too—the level of attention to detail. I think our waiter only had three or four other tables to wait on, and when he wasn't busy, he was standing nearby, waiting at attention. And if we was busy, his assistant (Assistant!) was nearby waiting to serve us.

And boy, did we get served. A cheese platter of assertive goat, a more assertive bleu and a brie that gave butter a run for its money. We decided not to get any caviar, but we did get a Soupe à L’Oignon gratinee for me and Pâté de Foie Gras for Bunny. Those were followed by a Petite Laitue Roquefort for me, and a Salade de Betterave for Bunny.

And then the entrees. I ordered the Risotto de Saint-Jacques à la truffe et aux épinards while Bunny had the Côte de veau aux Champignons Forresterie. I think the most amazing thing about Bunny's dinner was the gratin potatoes. Our waiter explained how they were made—the potatoes were sliced paper thin (thin enough to see through) and then layered with gruyere cheese, pressed down with a brick for over 24 hours in the refriderator, then baked and just before serving, deep fried for about a minute. It was perfect cube of potato and cheese about 3″ (7.5cm) per side, made up of easily a hundred layers.

Desert was a chocolate soufflé. That was my first soufflé, and also my last. I'm not saying it was bad—it wasn't by any stretch of the imagination—it was delicious! But my issue with the soufflé was one of texture. I'm sensitive to some … textures … that some foods have, and this one triggered that sensitivity. The only reason I ate more than one spoonfull was just down to how good it was, and I was able to bull through my textural revulsions. In all my times eating at find dining establishments, I've found that even if I don't like a particular item, it will be the best particular item I've ever had.

Was everything we had good? It's French! Of course it was good. All of it. The service was incredible, and our only real complaint of the night was the noise level when we first arrived—it was a bit too high for our liking. And the price was about what we expected—it's the “once a year” level of price.

Friday, January 26, 2024

“It is just a barebones framework written as a love letter to the language of C.”

I'm used to having some of my posts make it to the Orange Site, but I was not expecting to have one of my software projects (in this case, mod_blog) make it to the Orange Site. I'm finding it especially amusing to read this thread. I did not expect mod_blog to be a “love letter to C.”

Wednesday, January 31, 2024

The Repair Culture, Part II—Electric DeoxIToo

I've repaired my Logitech Trackman Marble a few times over the years, but this time the traditional fix didn't work. But I have seen enough Adrian Black computer repair videos to know that perhaps, a liberal application of DeoxIT Spray Contact Cleaner on the buttons might work. I ordered some last week, and when I went out to check the mail, I found it sitting on top of the mail box! I'm surprised it didn't fall off.

Anyway, a liberal amount of DeoxIT in the buttons and yup, that did the trick this time.

Sweet.


Dear LinkedIn, I don't think I'm the expert you want answering these questions

I cross post links to my blog on LinkedIn mainly to be contrarian, or at least think I'm providing something different than all the business related stuff posted there. But lately, I've seen notifications there like, “You're one of the few experts invited to add this collaborative article: …” and it's some article about AI, or Agile or the latest one, “Your team is falling behind on project deadlines. What web-based tools can help you catch up?”

Oh … nothing good can come from me answering these questions.


I know languages that have support for “read-only memory,” but what about “write-only memory?”

I'm still hacking away on my overengineered 6809 assembler and one feature I've beem mulling over is a form of static access checking. I have byte-level access control when running tests but I'm thinking that adding some form of “assemble time checking” would also be good. I've been writing code hitting the hardware of the Color Computer, and there are semantics around hardware that I don't think many languages (or even assembler) support—write only memory! As the Amiga Hardware Reference Manual states (only referenced here because it's a good example of what I'm talking about):

Registers are either read-only or write-only. Reading a write-only register will trash the register. Writing a read-only register will cause unexpected results.

When strobing any register which responds to either a read or a write, (for example copjmp2) be sure to use a MOVE.W, not CLR.W. The CLR instruction causes a read and a clear (two access) on a 68000, but only a single access on 68020 processors. This will give different results on different processors.

The Color Computer isn't quite as finicky (although the 6809 inside it also does the “read, then write” thing with the CLR instruction), but there is still memory-mapped IO that is read-only, some that is write-only, and some that has different meanings when reading and writing. And while C has some semantic support with volatile (to ensure reads and writes happen when stated) and const (to ensure the read-only nature) it still lacks a “write-only” concept. And I've never used an assembler that had “write-only” semantics either.

I'm thinking something along these lines:

		org	$FF40
DSK.CTRL	rmb/w	1	; write only

		org	$FF48
DSK.CMD		rmb/w	1	; write only
		org	$FF48
DSK.STATUS	rmb/r	1	; read only

DSK.TRACK	rmb	1	; these can be read and written
DSK.SECTOR	rmb	1
DSK.DATA	rmb	1

Here, the RMB directive just reserves a number of bytes, with a default access of “read-write.” The /W or /R designates that label as being either “write-only” or “read-only.” And if you look closely, you'll see that both DSK.CMD and DSK.STATUS are defined as the same address. It's just that writing to that address will send a command to the drive controller, while reading from that address give the current status. The only issue I have are hardware registers that can be programmed for input or output. The MC6821 used in the Color Computer has such registers, and the issue I have is how to signify this change in state in a program—that at this point in the program, such-n-such address is “read-write” but afterwards, it's “write-only.”


Discussions about this entry


How much for the book? I don't think I paid that much for the computer

Now, about that Amiga Hardware Reference Manual link in my previous post$577.99‽

Seriously‽

I know it's now a niche market, but are there really people buying that book for almost $600?

Good Lord!

Friday, February 02, 2024

Making it to the Orange Site

A previous post made it to Lobster only to later show up at the Orange Site. How about that?


Making it to the Orange Site

A previous post made it to Lobster only to later show up at the Orange Site. How about that?


Making it to the Orange Site

A previous post made it to Lobster only to later show up at the Orange Site. How about that?


It should be obvious by now what day it is, but just in case

I want to ensure everybody that yes, I intentionally made the previous three posts. And while they are similar, they are not in fact, identical. You know, much like Ground Hog Day.

Monday, February 05, 2024

The difficulties in supporting “write-only memory” in assembly

When I last wrote about this, I had one outstanding problem with static analysis of read-only/write-only memory, and that was with hardware that could be input or output only. It was only after I wrote that that I realized the solution—it's the same as a hardware register having different semantics on read vs. write—just define two labels with the semantics I want. So for the MC6821, I could have:

		org	$FF00
PIA0.A		rmb/r	1	; read only
		org	$FF00
PIA0.Adir	rmb/w	1	; write only, to set the direction of each IO pin
PIA0.Acontrol	rmb	1	; control for port A

So that was a non-issue. It was then I started looking over some existing code I had to see how it might look. I didn't want to just jump into an implementation without some forethought, and I quickly found some issues with the idea by looking at my maze generation program. The code in question initializes the required video mode (in this case 64×64 with four colors). Step one involves writing a particular value to the MC6821:

		lda	#G1C.PIA ; 64x64x4
		sta	PIA1.B

So far so good. I can mark PIA1.B as write-only (technically, it also has some input pins so I really can't, but in theory I could).

Now, the next bit requires some explaining. There's another 3-bit value that needs to be configured on the MC6883, but it's not as simple as writing the 3-bit value to a hardware register—each bit requires writing to a different address, and worse—it's a different address if the bit is 0 or 1. So that's six different addresses required. It's not horrible though—the addresses are sequential:

6883 VDG Addressing Mode
bit 0/1 address
V0 0 $FFC0
V0 1 $FFC1
V1 0 $FFC2
V1 1 $FFC3
V2 0 $FFC4
V2 1 $FFC5

Yeah, to a software programmer, hardware can be weird. To set bit 0 to 0, you do a write (and it does not matter what the value is) to address $FFC0. If bit 0 is 1, then it's a write to $FFC1. So with that in mind, I have:

		sta	SAM.V0 + (G1C.V & 1<<0 <> 0)
		sta	SAM.V1 + (G1C.V & 1<<1 <> 0)
		sta	SAM.V2 + (G1C.V & 1<<2 <> 0)

OOh. Yeah.

I wrote it this way so I wouldn't have to look up the appropriate value and write the more opaque (to me):

		sta	$FFC1
		sta	SFFC2
		sta	$FFC4

The expression (G1C.V & 1<<n <> 0) checks bit n to see if it's set or not, and returns 0 (for not set) or 1 (for set). This is then added to the base address for bit n, and it all works out fine. I can change the code for, say, the 128×192 four color mode by using a different constant:

		lda	#G6C.PIA
		sta	PIA1.B
		sta	SAM.V0 + (G6C.V & 1<<0 <> 0)
		sta	SAM.V1 + (G6C.V & 1<<1 <> 0)
		sta	SAM.V2 + (G6C.V & 1<<2 <> 0)

But I digress.

This is a bit harder to support. The address being written is part of an expression, and only the label (defining the address) would have the read/write attribute associated with it. At least, that was my intent. I suppose I could track the read/write attribute by address, which would solve this particular segment of code.

And the final bit of code to set the address of the video screen (or frame buffer):

		ldx	#SAM.F6		; point to frame buffer address bits
		lda	ECB.grpram	; get MSB of frame buffer
mapframebuf	clrb
		lsla
		rolb
		sta	b,x		; next bit of address
		leax	-2,x
		cmpx	#SAM.F0
		bhs	mapframebuf

Like the VDG Address Mode bits, the bits for the VDG Address Offset have unique addresses, and because the VDG Address Offset has seven bits, the address is aligned to a 512 byte boundary. Here, the code loads the X register with the address of the upper end of the VDG Address Offset, and the seven top most bits of the video address is sent, one at a time, to the B register, which is used as an offset to the X register to set the appropriate address for the appropriate bit. So now I would have to track the read/write attributes via the index registers as well.

That is not so easy.

I mean, here, it could work, as the code is all in one place, but what if instead it was:

		ldx	#SAM.F6
		lda	ECB.grpram
		jsr	mapframebuf

Or an even worse example:

costmessage	fcc/r	"A constant message" ; read only text
buffer		rmb	18

		ldx	#constmessage
		ldy	#buffer
		lda	#18
		jsr	memcpy

The subroutine memcpy might not even be in the same source unit, so how would the read/write attribute even be checked? This is for static analysis, not runtime.

I have one variation on the maze generation program that generates multiple mazes at the same time, on the same screen (it's fun to watch) and as such, I have the data required for each “maze generator” stored in a structure:

explorec	equ	0	; read-only
backtrackc	equ	1	; read-only
xmin		equ	2	; read-only
ymin		equ	3	; read-only
xstart		equ	4	; read-only
ystart		equ	5	; read-only
xmax		equ	6	; read-only
ymax		equ	7	; read-only
xpos		equ	8	; read-write
ypos		equ	9	; read-write
color		equ	10	; read-write
func		equ	11	; read-write

This is from the source code, but I've commented each “field” as being “read-only” or “read-write.” That's another aspect of this that I didn't consider:

		lda	explorec,x	; this is okay
		sta	explorec,x	; this is NOT okay

Not only would I have to track read/write attributes for addresses, but for field accesses to a structure as well. I'm not saying this is impossible, it's just going to take way more thought than I thought. I don't think I'll have this feature done any time soon …

Tuesday, February 06, 2024

So you want to amplify my SEO

From
Krystal XXXXX­XX <XXXXX­XXXXX­XXXXX­XXXXX@gmail.com>
To
sean@conman.org
Subject
Amplify Your SEO with Strategic Link Inserts
Date
Wed, 7 Feb 2024 01:16:35 +0300

Hi Content Team,

It’s Krystal XXXXX­XX here from Next Publisher, your next potential partner in digital storytelling. We're thrilled about the idea of featuring both guest posts and link insertions on your dynamic website.

We would like to know your fee structure for hosting guest posts and link insertions. Our team aims to create compelling content that is tailored to your site’s audience and enhances your overall content strategy.

A quick note: this initial email is only for starting our dialogue. All more detailed communications, including agreements and transactions, will be carried out through our official Next Publisher email.

We admire the quality of your platform and are excited to explore how we can work together for mutual benefit.

Looking forward to your prompt reply.

Warm wishes,

Krystal XXXXX­XX

Hello Krystal.

Since you neglected to include a link (in an email sent as HTML no less!) it's hard for me judge the value Next Publisher will provide for my site, so I'm going to have to adjust my prices accordingly. My fee for both guest posts and link insertions is $10,000 (US) per. So if a guest post also includes a link insertion, that would be a total of $20,000 (US). If I'm going to be whoring selling out what Google Page Rank I have, it's going to cost.

I look forward to hearing back from you.

Sean.


Okay! I'll answer your question, LinkedIn. Also, Orange Site! Orange Site! Orange Site!

Today's “you're one of the few experts invited to add this collaborative article” from LinkedIn is “How do you become a senior application developer?” My answer? Stay in a programming job for three years. Boom! You're a senior application developer. I know, I know, that's a big ask these days when everybody is jumping ship every two years. But hey, if you want to be a “senior application developer,” you got to make sacrifices.

Oh, and the title to today's post? I found out that LinkedIn really liked when I mentioned the Orange Site in the title of my post. Almost two orders of magnitude more. So I'm doing a test to see if I can game the system there.

Wednesday, February 07, 2024

Instead of “write-only memory” assembly support, how about floating point support?

You might think it odd to add support for floating point constants for an 8-bit CPU, but Motorola did development on the MC6839 floating point firmware for the MC6809, an 8K ROM of thread-safe, position-independent 6809 code that implements the IEEE Standard for Floating-Point Arithmetic. It was never formally released by Motorola as a product, but from what I understand, it was released later under a public domain license. At the very least, it's quite easy to MC6839 find both the ROM image and the source code on the Intarwebs. So that's one reason.

Another reason is that the Color Computer BASIC supports floating point operations, and while not IEEE-754, as it was written before the IEEE-754 standard become a standard, it still floating point, and there are only minor differences between it and the current standard, namely the exponent bias, number of fractional bits supported, and where the sign bit is stored. It really comes down to some bit manipulations to massage a standard float into the Color Computer BASIC float format. There are some differences, but the differences are small (literally, on the scale of 0.0000003) probably due to parsing differences, and small enough that it should be “good enough.” Especially since the Color Computer BASIC float format doesn't support infinity or NaN.

So if you specify a backend other than the rsdos backend, you get IEEE-754, and if you do specify rsdos as a backend, you get the Color Computer BASIC float format.

And yes, I added support for floating point expressions (but not for the test backend—I'm still thinking on how to support it), and one interesting feature I added is the factorial operator “!”. Factorials are used in Talor series, which the Color Computer BASIC uses for the sin() function, so I can literally write:

	; Oh!  '**' is exponentiation by the way!
taylor_series	.float	-((2 * 3.14159265358979323846) ** 11) / 11!
		.float	 ((2 * 3.14159265358979323846) **  9) /  9!
		.float	-((2 * 3.14159265358979323846) **  7) /  7!
		.float	 ((2 * 3.14159265358979323846) **  5) /  5!
		.float	-((2 * 3.14159265358979323846) **  3) /  3!
		.float	   2 * 3.14159265358979323846

and have it generate the correct values. I personally don't know of any language that has a factorial operator (maybe APL? I don't know).

I think I'm having more fun writing the assembler than I am writing assembly code.

Sunday, February 11, 2024

An extensible programming language

A few days ago I wrote about adding a factorial operator to my assembler, and I noted that I knew not of any other languages that had such a feature. So imagine my surprise as I'm reading about XL (via Lobsters) and the second example is factorial! Not only that, but that was an example of extending the language itself! The last time I was this excited about software was reading about Synthesis OS, a JIT-based operating system where you could create your own system calls.

How it handles precedence is interesting. In my assembler, I have left or right associativity as an explicit field, whereas in XL, it's encoded in the precedence level itself—even if its left, odd if its right. I'm not sure how I feel about that. On the one hand it feels nice and it's one less field to carry around; on the other, being explicit as I did makes it clear if something is left or right. But on the gripping hand, it sounds like matching precedence on a left and right operator could lead to problems, so I still may have an explicitness problem.

But I digress.

It's a very simple language with only one keyword “is” and a user-definable precedence table. The parser generates a parse tree of only eight types, four leaf nodes (integer, real, text, name (or symbol)) and four non-leaf nodes (prefix, infix, postfix and block). And from there, you get XL.

This is something I definitely want to look into.

Wednesday, February 14, 2024

Notes from an overheard conversation from a car attempting a right turn

“Oh! Now what?”

“Sir, see the lit sign up there? You cannot turn right.”

“But did you not see the car right in front of me turning right?”

“Sir, if a person jumped off a bridge, would you follow?”

“Yes.”

“You must be very smart then.”

“And selective enforcement of the laws leads to distrust of the police.”

“Sir—”

“Oh look! The ‘No Right Turn’ sign is off now! Gotta go! Bye!”

“I don't think it's wise to taunt the poice like that.”

“Down with the Man! Power to the people! Yo!”

Wednesday, February 28, 2024

Converting IEEE-754 floating point to Color BASIC floating point

I'm still playing around with floating point on the 6809—specifically, support for floating point for the Color Computer. The format for floating point for Color BASIC (written by Microsoft) predates the IEEE-754 Floating Point Standard by a few years and thus, isn't quite compatible. It's close, though. It's defined as an 8-bit exponent, biased by 129, a single sign bit (after the exponent) and 31 bits for the mantissa (the leading one assumed). It also does not support ±∞ nor NaN. This differs from the IEEE-754 single precision that uses a single sign bit, an 8-bit exponent biased by 127 and 23 bits for the mantissa (which also assumes a leafing one) and support for infinities and NaN. The IEEE-754 double precision uses a single sign bit, an 11-bit exponent biased by 1023 and 52 bit for the mantissa (leading one assumed) plus support for infinities and NaN.

So the Color BASIC is about halfway between single precision and double precision. This lead me to use IEEE-754 double precision for the Color Computer backend (generating an error for inifinities and NaN) then massaging the resulting double into the proper format. I double checked this by finding some floating point constants in the Color BASIC ROM as shown in the book Color BASIC Unravelled II, (available on the Computer Computer Archives), like this table:

4634				* MODIFIED TAYLOR SERIES SIN COEFFICIENTS
4635	BFC7 05			LBFC7	FCB	6-1			SIX COEFFICIENTS
4636	BFC8 84 E6 1A 2D 1B	LBFC8	FCB	$84,$E6,$1A,$2D,$1B	* -((2*PI)**11)/11!
4637	BFCD 85 28 07 FB F8	LBFCD	FCB	$86,$28,$07,$FB,$F8	*  ((2*PI)**9)/9!
4638	BFD2 87 99 68 89 01	LBFD2	FCB	$87,$99,$68,$89,$01	* -((2*PI)**7)/7!
4639	BFD7 87 23 35 DF E1	LBFD7	FCB	$87,$23,$35,$DF,$E1	*  ((2*PI)**5)/5!
4640	BFDC 86 A5 5D E7 28	LBFDC	FCB	$86,$A5,$5D,$E7,$28	* -((2*PI)**3)/3!
4641	BFE1 83 49 0F DA A2	LBFE1	FCB	$83,$49,$0F,$DA,$A2	*    2*PI

Then using the byte values to populate a variable and printing it inside BASIC (this is the expression -2π3/3!):

X=0         ' CREATE A VARIABLE
Y=VARPTR(X) ' GET ITS ADDRESS
POKE Y,&H86 ' AND SET ITS VALUE
POKE Y+1,&HA5 ' THE HARD WAY
POKE Y+2,&H5D
POKE Y+3,&HE7
POKE Y+4,&H28
PRINT X ' LET'S SEE WHAT IT IS
-41.3417023

Then using that to create a floating point value:

	org	$1000
	.float	-41.3417023
	end

Checking the resulting bytes that were generated:

                         | FILE ff.a
                       1 |         org     $1000
1000: 86A55DE735       2 |         .float  -41.3417023
                       3 |         end

And adjusting the floating point constant until I got bytes that matched:

                         | FILE ff.a
                       1 |         org     $1000
1000: 86A55DE728       2 |         .float  -41.341702110
                       3 |         end

I figure it's “close enough.” The parsing code in the Color BASIC ROM is old and predates the IEEE-754 floating point standard, so a few different digits at the end I think is okay.

As a final check, I wrote the following bit of code to calculate and display -2π3/3!, display the pre-calculated result, as well as display the pre-calculated value of 2π:

		include	"Coco/basic.i"
		include	"Coco/dp.i"

CB.FSUBx	equ	$B9B9	; FP0 = X   - FP0	; addresses for
CB.FSUB		equ	$B9BC	; FP0 = FP1 - FP0	; these routines 
CB.FADDx	equ	$B9C2	; FP0 = X   + FP0	; from
CB.FADD		equ	$B9C5	; FP0 = FP1 + FP1	; Color BASIC Unravelled II
CB.FMULx	equ	$BACA	; FP0 = X   * FP0
CB.FMUL		equ	$BAD0	; FP0 = FP0 * FP1
CB.FDIVx	equ	$BB8F	; FP0 = X   / FP0
CB.FDIV		equ	$BB91	; FP0 = FP1 / FP0

CB.FP0fx	equ	$BC14	; FP0 = X
CB.xfFP0	equ	$BC35	; X   = FP0
CB.FP1f0	equ	$BC5F	; FP1 = FP0
CB.FP0txt	equ	$BDD9	; result in X, NUL terminated

		org	$4000
start		ldx	#tau		; point to 2*pi
		jsr	CB.FP0fx	; copy to FP0
		ldx	#tau		; 2PI * 2PI
		jsr	CB.FMULx
		ldx	#tau		; 2PI * 2PI * 2PI
		jsr	CB.FMULx
		jsr	CB.FP1f0	; copy fp acc to FP1
		ldx	#fact3		; point to 3!
		jsr	CB.FP0fx	; copy to FP0
		jsr	CB.FDIV		; FP0 = FP1 / FP0
		neg	CB.fp0sgn	; negate result by flippping FP0 sign
		jsr	CB.FP0txt	; generate string
		bsr	display		; display on screen

		ldx	#answer		; point to precalculated result
		jsr	CB.FP0fx	; copy to FP0
		jsr	CB.FP0txt	; generate string
		bsr	display		; display

		ldx	#tau		; now display 2*pi
		jsr	CB.FP0fx	; just to see how close
		jsr	CB.FP0txt	; it is.
		bsr	display
		rts

display.char	jsr	[CHROUT]	; display character
display		lda	,x+		; get character
		bne	.char		; if not NUL byte, display
		lda	#13		; go to next line
		jsr	[CHROUT]
		rts

tau		.float	6.283185307
fact3		.float	3!
answer		.float	-(6.283185307 ** 3 / 3!)

		end	start

The results were:

-41.3417023
-41.3417023
 6.23418531

The calculation results in -41.3417023 and the direct result stored in answer also prints out -41.3417023, so that matches and it reinforces my approach to this nominally right.

But I think Microsoft had issues with either generating some of the floating point constants for the larger terms, or transcribing the byte values of the larger terms. Take for instance -2π11/11!. The correct answer is -15.0946426, but the bytes in the ROM define the constant -14.3813907, a difference of .7. And it's not like Color BASIC can't calculate that correctly—when I typed in the expression by hand, it was able to come up with -15.0946426.

Or it could be that Walter K. Zydhek, the author of Color BASIC Unravelled II, is wrong in his interpretation of the expressions used to generate the values, or his interpretation of what the values are used for. I'm not sure who is at fault here.

Update on Friday, March 1st, 2024

I was wrong about the authorship of Color BASIC Unravelled II. It was not Walter K. Zydhek, but some unknown author of Spectral Associates, a company that is no longer in business. All Zydhek did was to transcribe a physical copy of the book (which is no longer available for purchase anywhere) into a PDF and make it available.


Discussions about this entry

Friday, March 01, 2024

The speed of Microsoft's BASIC floating point routines

I was curious about how fast Microsoft's BASIC floating point routines were. This is easy enough to test, now that I can time assembly code inside the assembler. The code calculates -2π3/3! using Color BASIC routines, IEEE-754 single precision and double precision.

First, Color BASIC:

	.tron	timing
ms_fp		ldx	#.tau
		jsr	CB.FP0fx	; FP0 = .tau
		ldx	#.tau
		jsr	CB.FMULx	; FP0 = FP0 * .tau
		ldx	#.tau
		jsr	CB.FMULx	; FP0 = FP0 * .tau
		jsr	CB.FP1f0	; FP1 = FP0
		ldx	#.fact3
		jsr	CB.FP0fx	; FP0 = 3!
		jsr	CB.FDIV		; FP0 = FP1 / FP0
		neg	CB.fp0sgn	; FP0 = -FP0
		ldx	#.answer
		jsr	CB.xfFP0	; .answer = FP0
	.troff
		rts

.tau		fcb	$83,$49,$0F,$DA,$A2 
.fact3		fcb	$83,$40,$00,$00,$00  
.answer		rmb	5
		fcb	$86,$A5,$5D,$E7,$30	; precalculated result

I can't use the .FLOAT directive here since that only supports either the Microsoft format or IEEE-754 but not both. So for this test, I have to define the individual bytes per float. The last line is what the result should be (by checking a memory dump of the VM after running). Also, .tao is , just in case that wasn't clear. This ran in 8,742 cycles, taking 2,124 instructions and 4.12 cycles per instruction (I modified the assembler to record this additional information).

Next up, IEEE-754 single precision:

	.tron	timing
ieee_single	ldu	#.tau
		ldy	#.tau
		ldx	#.answer
		ldd	#.fpcb
		jsr	REG
		fcb	FMUL	; .answer = .tau * .tau

		ldu	#.tau
		ldy	#.answer
		ldx	#.answer
		ldd	#.fpcb
		jsr	REG
		fcb	FMUL	; .answer = .answer * .tau

		ldu	#.answer
		ldy	#.fact3
		ldx	#.answer
		ldd	#.fpcb
		jsr	REG
		fcb	FDIV	; .answer = .answer / 3!

		ldy	#.answer
		ldx	#.answer
		ldd	#.fpcb
		jsr	REG
		fcb	FNEG	; .answer = -.answer
	.troff
		rts

.fpcb		fcb	FPCTL.single | FPCTL.rn | FPCTL.proj
		fcb	0
		fcb	0
		fcb	0
		fdb	0

.tau		.float	6.283185307
.fact3		.float	3!
.answer		.float	0
		.float	-(6.283185307 ** 3 / 3!)

The floating point control block (.fpcb) configures the MC6839 to use single precision, normal rounding and projective closure (not sure what that is, but it's the default value). And it does calculate the correct result. It's amazing that code written 42 years ago for an 8-bit CPU works flawlessly. What it isn't is fast. This code took 14,204 cycles over 2,932 instructions (average 4.84 cycles per instruction).

The higher than average cycle type could be due to position independent addressing modes, but I'm not entirely sure what it's doing to take nearly twice the time. The ROM does use the IEEE-754 extended format (10 bytes) internally, with more bit shifts to extract the exponent and mantissa, but twice the time?

Perhaps it's code to deal with ±∞ and NaNs.

The IEEE-754 double precision is the same, except for the floating point control block configuring double precision and the use of .FLOATD instead of .FLOAT; otherwise the code is identical. The result, however, isn't. It took 31,613 cycles over 6,865 instructions (average 4.60 cycles per instruction). And being twice the size, it took nearly twice the time as single precision, which is expected.

The final bit of code just loads the ROMs into memory, and calls each function to get the timing:

		org	$2000
		incbin	"mc6839.rom"
REG		equ	$203D	; register-based entry point

		org	$A000
		incbin	"bas12.rom"

	.opt	test	prot	rw,$00,$FF	; Direct Page for BASIC
	.opt	test	prot	rx,$2000,$2000+8192 ; MC6839 ROM
	.opt	test	prot	rx,$A000,$A000+8192 ; BASIC ROM

	.test	"BASIC"
		lbsr	ms_fp
		rts
	.endtst

	.test	"IEEE-SINGLE"
		lbsr	ieee_single
		rts
	.endtst

	.test	"IEEE-DOUBLE"
		lbsr	ieee_double
		rts
	.endtst

Really, the only surprising thing here was just how fast Microsoft BASIC was at floating point.

Monday, April 01, 2024

Notes on an overheard conversion while eating dinner at The Cracker Barrel

“Oh no. This is bad.”

“Wow! Are you sure there are no more moves you can make?”

“Nope. See?”

“How many pegs is that?”

“10.”

“Wow! You are really bad at that!”

[If you add up all the possible ways leaving just one peg in the Peg Game, including rotations, reflections and reflected rotations, you have 438,984 ways of solving the Peg Game. If you add up all the possible ways of leaving 10 pegs, including rotations, reflections and reflected rotations, you have just six solutions. It is a much harder problem leaving 10 pegs than leaving one. I'm just saying … —Editor]

Tuesday, April 02, 2024

It only took 25 years for my idea to catch on

I was exchanging emails with Christian about online document structure when I mentioned The Electric King James Bible and it's rather unique addressing scheme. I came up with that 25 years ago [Good Lord! Has it been that long? —Sean] [Yes. —Editor] [Yikes! —Sean] to precisely pull up Bible verses—anywhere from one verse to an entire book. Of all the Bible sites across the Intarwebs I've come across since have never used such an elegant, and to me, obvious, way of referencing the Bible online. Usually they use a URL format like <https://bible.example.org/?bible=kj&book=Genesis&chapter=1&start_verse=1&end_verse=1>.

But Christian mentioned Sefaria as using my method, and true enough, it does! <https://www.sefaria.org/Genesis.6:9-9:17> does indeed go to the Noah's Ark story. I think that's neat! I don't know if they were inspired by my site (unlikely, but not completely out of the relms of possibility) or just came up with it on their own, but it's nice to see someone else is using an easy to hack URL form for Bible references.

There are differences though—my site only brings up the requested material, whereas Sefaria implements a bidirectional “Scroll Of Doom” where additional material appears when you go up or down. I can't say I'm a fan of that, but it apparently works for them.


Dear LinkedIn, why are you still asking me these questions?

LinkedIn is still asking me to participate as an expert answering questions—this time, “You're a system architect. How do you decide which programming languages to learn?” And just below that is “Powered by AI and the LinkedIn community.”

Sigh. Eu tu, LinkedIn?

I'm still tempted to answer, but no. I can't just bear to answer this how I would want to answer it. Besides, if you know where to look, you might find my answers anyway.

Wednesday, April 03, 2024

An excessive number of packaging layers

I ordered an item from Amazon the other day. The expected arrival time was Friday, but instead, it arrived today. On the front porch was an Amazon box, measuring 6″ × 9″ × 5″ (16cm × 23cm × 13cm for the more civilized amongst you). Inside was another box, 3″ × 4½″ × ⅜″ (7cm × 11cm × 1cm). Inside that was a slightly smaller anti-static bag. Inside that was a smaller plastic bad, and finally, inside that was the item I had purchased—a replacement battery for my old-school flip phone.

Seriously? Four layers of packaging? Sigh.


“Because this kind of battery is encrypted …”

So I'm reading the “Battery Replacement Installation Manual” for the battery I just bought and as translated instructions go, it's not that bad. But there are some choice bits though …

Why does the phone echo?

The echo of the phone may be due to the installation problem. Can you see if there are any loose parts, because the battery will not affect the quality of the phone's call unless there is no power and cause the phone shut down.

“The echo of the phone?”

Feedback? Hearing my own voice echoed back to me? Maybe?

Anyway, carrying on …

Why did I receive a swollen battery?

Because this kind of battery is encrypted …

I have no clue here. It states that swelling may occur if the temperature exceeds 158°F (70°C), and enter sleep mode if the temperature is too low, although it doesn't state what “too low” means. Fortunately, the battery I received isn't swollen, so I guess it's not encrypted?

4. Please carefully check whether there is any debris or screws falling into the battery area. If there is, please clean it up before proceeding to the next step, otherwise the sundries may pierce the battery and cause a short circuit and cause spontaneous combustion.

“Sundries.” Love it!

Thursday, April 04, 2024

Tracking down a bug

I've spent the past two days tracking down a bug, and I think it's a library issue.

So I have this program I wrote some time ago that uses Xlib and for reasons, I needed to store a 64-bit value that's related to a window. This is easy enough with setting a window property. The code for that is easy enough:

void svalue(Display *display,Window window,unsigned long long int value)
{
  assert(display != NULL);
  assert(window  != None);

  XChangeProperty(
    display,
    window,
    CALC_VALUE,
    XA_INTEGER,
    32,	/* format */
    PropModeReplace,
    (unsigned char *)&value,
    sizeof(value) / 4 /* number of 'format' units */
  );
}

CALC_VALUE is the “variable” (for lack of a better term) and XA_INTEGER is (again, for lack of a better term) the base type. Yes, this is just wrapping a single function call in a function, but it's an abstraction to make things simpler as it's called from multiple locations in the codebase.

To query the value:

unsigned long long int qvalue(Display *display,Window window)
{
  assert(display != NULL);
  assert(window  != None);
  
  unsigned long long int  value;
  Atom                    atom_got;
  unsigned char          *plong;
  int                     rc = XGetWindowProperty(
                                  display,
                                  window,
                                  CALC_VALUE,
                                  0,
                                  sizeof(unsigned long long int) / 4,
                                  False,
                                  XA_INTEGER,
                                  &atom_got,
                                  &(int){0}, /* this is don't care */
                                  &(unsigned long int){0}, /* another don't care */
                                  &(unsigned long int){0}, /* another don't care */
                                  &plong
                                );
                     
  if ((rc == Success) && (atom_got == XA_INTEGER))
  {
    memcpy(&value,plong,sizeof(unsigned long long int));
    XFree(plong);
  }
  else
    value = 0;
    
  return value;
}

Again, nothing too horrible or tricky.

The code was originally written on a 32-bit system (just after I left The Enterprise), and it worked. I then wanted to get the program working on a 64-bit system (beacuse I want to both release it and talk about it). It worked, but only for values of 31-bits or less. As soon as the value hit 32-bits, the upper 32-bits were all 1s.

I added code to dump the value just before the call to XChangeProperty() and code to dump the value just after the call to XGetWindowProperty() and somewhere, once the value was 0x00000000FFFFFFFF going into XChangeProperty(), it was 0xFFFFFFFFFFFFFFFF coming out of XGetWindowProperty().

32-bit version? No issues. 64-bit version? Issues.

I tried a different compiler, on the off chance that I might be hitting some weird compiler bug, and no go, GCC or Clang, both on the 64-bit system had the same issue. I tried using a different X server and the same results—32 bit client, fine; 64-bit client, not fine. So I think it's due to the client side on the 64-bit system where the issue lies. Also, if I change the call to XChangeProperty() to:

void svalue(Display *display,Window window,unsigned long long int value)
{
  assert(display != NULL);
  assert(window  != None);

  XChangeProperty(
    display,
    window,
    CALC_VALUE,
    XA_INTEGER,
    8, /* format, this time 8! */
    PropModeReplace,
    (unsigned char *)&value,
    sizeof(value) /* still number of 'format' units */
  );
}

That is, a format of 8 fixed the issue. Even a format of 16 worked. It's just that when I try to use a format of 32, on the 64-bit system, does it fail.

And using a format of 8 on the 32-bit system works as well, so at least I have a workaround for it. Still, it's annoying.


I love it when abstractions are too abstract to be useful

I recently found an annoying aspect of Xlib—it's hard to find documentation about what keys affect the state field of the keyboard event. It's obvious that the shift keys on the keyboard will set ShiftMask, the control key will set ControlMask, and the CapsLock key will set LockMask (when I would expect it to set ShiftMask since it's just locking the shift keys to “on”), but there's little to say what keys set the Mod1Mask, Mod2Mask, Mod3Mask, Mod4Mask and Mod5Mask.

This is problematic, because I do need to check for keyboard events and this threw me for a loop—why are none of the keys working? Well, that's because my virtual Linux server on the Mac sets the NumLock key, which causes the X server to then set the Mod2Mask for all keyboard events and I wasn't expecting that.

Sigh.

Friday, April 05, 2024

Matchbox cars seem to have gotten bigger in recent years

Bunny and I went to a local Toyota dealership to fix an issue with her car (it turns out it was a very unusual, but very minor, issue) and while there, we saw this on the display floor:

[A very small electric car for one] That's not a car!  That's an oversized roller skate!

Turns out, this is not a large Matchbox car, but a small electric car straight from a factory in Japan (the informational flying under the windsheid is all in Japanese). A five year old would barely fit in this thing, much less an adult. There doesn't appear to be any storage space of any significant size, and sans doors, I'm not sure this is even road legal. And the the staff there don't even know if it's for sale. Weird.

Obligatory Picture

[The future's so bright, I gotta wear shades]

Obligatory Contact Info

Obligatory Feeds

Obligatory Links

Obligatory Miscellaneous

You have my permission to link freely to any entry here. Go ahead, I won't bite. I promise.

The dates are the permanent links to that day's entries (or entry, if there is only one entry). The titles are the permanent links to that entry only. The format for the links are simple: Start with the base link for this site: https://boston.conman.org/, then add the date you are interested in, say 2000/08/01, so that would make the final URL:

https://boston.conman.org/2000/08/01

You can also specify the entire month by leaving off the day portion. You can even select an arbitrary portion of time.

You may also note subtle shading of the links and that's intentional: the “closer” the link is (relative to the page) the “brighter” it appears. It's an experiment in using color shading to denote the distance a link is from here. If you don't notice it, don't worry; it's not all that important.

It is assumed that every brand name, slogan, corporate name, symbol, design element, et cetera mentioned in these pages is a protected and/or trademarked entity, the sole property of its owner(s), and acknowledgement of this status is implied.

Copyright © 1999-2024 by Sean Conner. All Rights Reserved.