Full Width [alt+shift+f] Shortcuts [alt+shift+k]
Sign Up [alt+shift+s] Log In [alt+shift+l]
32
My favorite memory of my M1 Pro MacBook Pro was the whole sensation of “holy crap, you never hear the fans in this thing”, which was very novel in 2021. Four years later, this MacBook Pro is still a delight. It’s the longest I’ve ever owned a laptop, and while I’d love to pick up the new M4 goodness, this dang thing still seems to just shrug at basically anything I throw at it. Video editing, code compiling, CAD models, the works. (My desire to update is helped though by the fact I got the 2TB SSD, 32GB RAM option, and upgrading to those on new MacBooks is still eye wateringly expensive.) But my MacBook is starting to show its age in one area: it’s not quiet anymore. If you’re doing anything too intensive like compiling code for awhile, or converting something in Handbrake, the age of the fans being quiet is long past. The fans are properly loud. (And despite having two cats, it’s not them! I clean out the fans pretty regularly.) Enter the thermal paste Everyone online seems to point...
3 weeks ago

Improve your reading experience

Logged in users get linked directly to articles resulting in a better reading experience. Please login for free, it takes less than 1 minute.

More from Christian Selig

High quality, low filesize GIFs

While the GIF format is a little on the older side, it’s still a really handy format in 2025 for sharing short clips where an actual video file might have some compatibility issues. For instance, I find when you just want a short little video on your website, a GIF is still so handy versus a video, where some browsers will refuse to autoplay them, or seem like they’ll autoplay them fine until Low Battery Mode is activated, etc. With GIFs it’s just… easy, and sometimes easy is nice. They’re super handy for showing a screen recording of a cool feature in your app, for instance. What’s not nice is the size of GIFs. They have a reputation of being absolutely enormous from a filesize perspective, and they often are, but that doesn’t have to be the case, you can be smart about your GIF and optimize its size substantially. Over the years I’ve tried lots of little apps that promise to help to no avail, so I’ve developed a little script to make this easier that I thought might be helpful to share. Naive approach Let’s show where GIFs get that bad reputation so we can have a baseline. We’ll use trusty ol’ ffmpeg (in the age of LLMs it is a super handy utility), which if you don’t have already you can install via brew install ffmpeg. It’s a handy (and in my opinion downright essential) tool for doing just about anything with video. For a video we’ll use this cute video of some kittens I took at our local animal shelter: It’s 4K, 30 FPS, 5 seconds long, and thanks to its H265/HEVC video encoding it’s only 19.5 MB. Not bad! Let’s just chuck it into ffmpeg and tell it to output a GIF and see how it does. ffmpeg -i kitties.mp4 kitties.gif Okay, let that run and- oh no. For your sake I’m not even going to attach the GIF here in case folks are on mobile data, but the resulting file is 409.4MB. Almost half a gigabyte for a 5 second GIF of kittens. We gotta do better. Better We can do better. Let’s throw a bunch of confusing parameters at ffmpeg (that I’ll break down) to make this a bit more manageable. ffmpeg -i kitties.mp4 -filter_complex "fps=24,scale=iw*sar:ih,scale=1000:-1,split[a][b];[a]palettegen[p];[b][p]paletteuse=dither=floyd_steinberg" kitties2.gif Okay, lot going on here, let’s break it down. fps=24: we’re dropping down to 24 fps from 30 fps, many folks upload full YouTube videos at this framerate so it’s more than acceptable for a GIF. scale=iw*sar:ih: sometimes video files have weird situations where the aspect ratio of each pixel isn’t square, which GIFs don’t like, so this is just a correction step so that doesn’t potentially trip us up scale=1000:-1: we don’t need our GIF to be 4K, and I’ve found 1,000 pixels across to be a great middle ground for GIFs. The -1 at the end just means scale the height to the appropriate value rather than us having to do the math ourselves. The rest is related to the color palette, we’re telling ffmpeg to scan the entire video to build an appropriate color palette up, and to use the Floyd-Steinberg algorithm to do so. I find this algorithm gives us the highest quality output (which is also handy for compressing it more in further steps) This gives us a dang good looking GIF that clocks in at about 10% the file size at 45.8MB. Link to GIF in lieu of embedding directly Nice! Even better ffmpeg is great, but where it’s geared toward videos it doesn’t do every GIF optimization imaginable. You could stop where we are and be happy, but if you want to shave off a few more megabytes, we can leverage gifsicle, a small command line utility that is built around optimizing GIFs. We’ll install gifsicle via brew install gifsicle and throw our GIF into it with the following: gifsicle -O3 --lossy=65 --gamma=1.2 kitties2.gif -o kitties3.gif So what’s going on here? O3 is essentially gifsicle’s most efficient mode, doing fancy things like delta frames so changes between frames are stored rather than each frame separately lossy=65 defines the level of compression, 65 has been a good middle ground for me (200 I believe is the highest compression level) gamma=1.2 is a bit confusing, but essentially the gamma controls how the lossy parameter reacts to (and thus compresses) colors. 1 will allow it to be quite aggressive with colors, while 2.2 (the default) is much less so. Through trial and error I’ve found 1.2 causes nice compression without much of a loss in quality The resulting GIF is now 23.8MB, shaving a nice additional 22MB off, so we’re now at a meagre 5% of our original filesize. That’s a lot closer to the 4K, 20MB input, so for a GIF I’ll call that a win. And for something like a simpler screen recording it’ll be even smaller! Make it easy Rather than having to remember that command or come back here and copy paste it all the time, add the following to your ~/.zshrc (or create it if you don’t have one already): gifify() { # Defaults local lossy=65 fps=24 width=1000 gamma=1.2 while [[ $# -gt 0 ]]; do case "$1" in --lossy) lossy="$2"; shift 2 ;; --fps) fps="$2"; shift 2 ;; --width) width="$2"; shift 2 ;; --gamma) gamma="$2"; shift 2 ;; --help|-h) echo "Usage: gifify [--lossy N] [--fps N] [--width N] [--gamma VAL] <input video> <output.gif>" echo "Defaults: --lossy 65 --fps 24 --width 1000 --gamma 1.2" return 0 ;; --) shift; break ;; --*) echo "Unknown option: $1" >&2; return 2 ;; *) break ;; esac done if (( $# < 2 )); then echo "Usage: gifify [--lossy N] [--fps N] [--width N] [--gamma VAL] <input video> <output.gif>" >&2 return 2 fi local in="$1" local out="$2" local tmp="$(mktemp -t gifify.XXXXXX).gif" trap 'rm -f "$tmp"' EXIT echo "[gifify] FFmpeg: starting encode → '$in' → temp GIF (fps=${fps}, width=${width})…" if ! ffmpeg -hide_banner -loglevel error -nostats -y -i "$in" \ -filter_complex "fps=${fps},scale=iw*sar:ih,scale=${width}:-1,split[a][b];[a]palettegen[p];[b][p]paletteuse=dither=floyd_steinberg" \ "$tmp" then echo "[gifify] FFmpeg failed." >&2 return 1 fi echo "[gifify] FFmpeg: done. Starting gifsicle (lossy=${lossy}, gamma=${gamma})…" if ! gifsicle -O3 --gamma="$gamma" --lossy="$lossy" "$tmp" -o "$out"; then echo "[gifify] gifsicle failed." >&2 return 1 fi local bytes bytes=$(stat -f%z "$out" 2>/dev/null || stat -c%s "$out" 2>/dev/null || echo "") if [[ -n "$bytes" ]]; then local mb mb=$(LC_ALL=C printf "%.2f" $(( bytes / 1000000.0 ))) echo "[gifify] gifsicle: done. Wrote '$out' (${mb} MB)." else echo "[gifify] gifsicle: done. Wrote '$out'." fi } This will allow you to easily call it as either gifify <input-filename.mp4> <output-gifname.gif> and default to the values above, or if you want to tweak them you can use any optional parameters with gifify --fps 30 --gamma 1.8 --width 600 --lossy 100 <input-filename.mp4> <output-gifname.gif>. For instance: # Using default values we used above gifify cats.mp4 cats.gif # Changing the lossiness and gamma gifify --lossy 30 --gamma 2.2 cats.mp4 cats.gif Much easier. May your GIFs be beautiful and efficient.

3 days ago 4 votes
A slept on upscaling tool for macOS

I uploaded YouTube videos from time to time, and a fun comment I often get is “Whoa, this is in 8K!”. Even better, I’ve had comments from the like, seven people with 8K TVs that the video looks awesome on their TV. And you guessed it, I don’t record my videos in 8K! I record them in 4K and upscale them to 8K after the fact. There’s no shortage of AI video upscaling tools today, but they’re of varying quality, and some are great but quite expensive. The legendary Finn Voorhees created a really cool too though, called fx-upscale, that smartly leverages Apple’s built-in MetalFX framework. For the unfamiliar, this library is an extensive of Apple’s Metal graphics library, and adds functionality similar to NVIDIA’s DLSS where it intelligently upscales video using machine learning (AI), so rather than just stretching an image, it uses a model to try to infer what the frame would look like at a higher resolution. It’s primarily geared toward video game use, but Finn’s library shows it does an excellent job for video too. I think this is a really killer utility, and use it for all my videos. I even have a license for Topaz Video AI, which arguably works better, but takes an order of magnitude longer. For instance my recent 38 minute, 4K video took about an hour to render to 8K via fx-upscale on my M1 Pro MacBook Pro, but would take over 24 hours with Topaz Video AI. # Install with homebrew brew install finnvoor/tools/fx-upscale # Outputs a file named my-video Upscaled.mov fx-upscale my-video.mov --width 7680 --codec h265 Anyway, just wanted to give a tip toward a really cool tool! Finn’s even got a [version in the Mac App Store called Unsqueeze](https://apps.apple.com/ca/app/unsqueeze/id6475134617 Unsqueeze) with an actual GUI that’s even easier to use, but I really like the command line version because you get a bit more control over the output. 8K is kinda overkill for most use cases, so to be clear you can go from like, 1080p to 4K as well if you’re so inclined. I just really like 8K for the future proofing of it all, in however many years when 8K TVs are more common I’ll be able to have some of my videos already able to take advantage of that. And it takes long enough to upscale that I’d be surprised to see TVs or YouTube offering that upscaling natively in a way that looks as good given the amount of compute required currently. Obviously very zoomed in to show the difference easier If you ask me, for indie creators, even when 8K displays are more common, the future of recording still probably won’t be in native 8K. 4K recording gives so much detail still that have more than enough details to allow AI to do a compelling upscale to 8K. I think for my next camera I’m going to aim for recording in 6K (so I can still reframe in post), and then continue to output the final result in 4K to be AI upscaled. I’m coming for you, Lumix S1ii.

a month ago 34 votes
Embedding Godot games in iOS is easy

Recently there’s been very exciting developments in the Godot game engine, that have allowed really easy and powerful integration into an existing normal iOS or Mac app. I couldn’t find a lot of documentation or discussion about this, so I wanted to shine some light on why this is so cool, and how easy it is to do! What’s Godot? For the uninitiated, Godot is an engine for building games, other common ones you might know of are Unity and Unreal Engine. It’s risen in popularity a lot over the last couple years due to its open nature: it’s completely open source, MIT licensed, and worked on in the open. But beyond that, it’s also a really well made tool for building games with (both 2D and 3D), with a great UI, beautiful iconography, a ton of tutorials and resources, and as a bonus, it’s very lightweight. I’ve had a lot of fun playing around with it (considering potentially integrating it into Pixel Pals), and while Unity and Unreal Engine are also phenomenal tools, Godot has felt lightweight and approachable in a really nice way. As an analogy, Godot feels closer to Sketch and Figma whereas Unity and Unreal feel more like Photoshop/Illustrator or other kinda bulky Adobe products. Even Apple has taken interest in it, contributing a substantial pull request for visionOS support in Godot. Why use it with iOS? You’ve always been able to build a game in Godot and export it to run on iOS, but recently thanks to advancements in the engine and work by amazing folks like Miguel de Icaza, you can now embed a Godot game in an existing normal SwiftUI or UIKit app just as you would an extra UITextView or ScrollView. Why is this important? Say you want to build a game or experience, but you don’t want it to feel just like another port, you want it to integrate nicely with iOS and feel at home there through use of some native frameworks and UI here and there to anchor the experience (share sheets, local notifications, a simple SwiftUI sheet for adding a friend, etc.). Historically your options have been very limited or difficult. You no longer have to have “a Godot game” or “an iOS app”, you can have the best of both worlds. A fun game built entirely in Godot, while having your share sheets, Settings screens, your paywall, home screen widgets, onboarding, iCloud sync, etc. all in native Swift code. Dynamically choosing which tool you want for the job. (Again, this was technically possible before and with other engines, but was much, much more complicated. Unity’s in particular seems to have been last updated during the first Obama presidency.) And truly, this doesn’t only benefit “game apps”. Heck, if the user is doing something that will take awhile to complete (uploading a video, etc.) you could give them a small game to play in the interim. Or just for some fun you could embed a little side scroller easter egg in one of your Settings screens to delight spelunking users. Be creative! SpriteKit? A quick aside. It wouldn’t be an article about game dev on iOS without mentioning SpriteKit, Apple’s native 2D game framework (Apple also has SceneKit for 3D). SpriteKit is well done, and actually what I built most of Pixel Pals in. But it has a lot of downsides versus a proper, dedicated game engine: Godot has a wealth of tutorials on YouTube and elsewhere, bustling Discord communities for help, where SpriteKit being a lot more niche can be quite hard to find details on The obvious one: SpriteKit only works on Apple platforms, so if you want to port your game to Android or Windows you’re probably not going to have a great time, where Godot is fully cross platform Godot being a full out game engine has a lot more tools for game development than can be handy, from animation tools, to sprite sheet editors, controls that make experimenting a lot easier, handy tools for creating shaders, and so much more than I could hope to go over in this article. If you ever watch a YouTube video of someone building a game in a full engine, the wealth of tools they have for speeding up development is bonkers. Godot is updated frequently by a large team of employees and volunteers, SpriteKit conversely isn’t exactly one of Apple’s most loved frameworks (I don’t think it’s been mentioned at WWDC in years) and kinda feels like something Apple ins’t interested in putting much more work into. Maybe that’s because it does everything Apple wants and is considered “finished” (if so I think that would be incorrect, see previous point for many things that it would be helpful for SpriteKit to have), but if you were to encounter a weird bug I’d feel much better about the likelihood of it getting fixed in Godot than SpriteKit I’m a big fan of using the right tool for the job. For iOS apps, most of the time that’s building something incredible in SwiftUI and UIKit. But for building a game, be it small or large, using something purpose built to be incredible at that seems like the play to me, and Godot feels like a great candidate there. Setup Simply add the SwiftGodotKit package to your Xcode project by selecting your project in the sidebar, ensuring your project is selected in the new sidebar, selecting the Package Dependencies tab, click the +, then paste the GitHub link. After adding it, you will also need to select the target that you added it to in the sidebar, select the Build Settings tab, then select “Other Linker Flags” and add -lc++. Lastly, with that same target, under the General tab add MetalFX.framework to Frameworks, Libraries, and Embedded Content. (Yeah you got me, I don’t know why we have to do that.) After that, you should be able to import SwiftGodotKit. Usage Now we’re ready to use Godot in our iOS app! What excites me most and I want to focus on is embedding an existing Godot game in your iOS app and communicating back and forth with it from your iOS app. This way, you can do the majority of the game development in Godot without even opening Xcode, and then sprinkle in delightful iOS integration by communicating between iOS and Godot where needed. To start, we’ll build a very simple game called IceCreamParlor, where we select from a list of ice cream options in SwiftUI, which then gets passed into Godot. Godot will have a button the user can tap to send a message back to SwiftUI with the total amount of ice cream. This will not be an impressive “game” by any stretch of the imagination, but should be easy to set up and understand the concepts so you can apply it to an actual game. To accomplish our communication, in essence we’ll be recreating iOS’ NotificationCenter to send messages back and forth between Godot and iOS, and like NotificationCenter, we’ll create a simple singleton to accomplish this. Those messages will be sent via Signals. This is Godot’s system for, well, signaling an event occurred, and can be used to signify everything from a button press, to a player taking damage, to a timer ending. Keeping with the NotificationCenter analogy, this would the be Notification that gets posted (except in Godot, it’s used for everything, where in iOS land you really wouldn’t use NotificationCenter for a button press.) And similar to Notification that has a userInfo field to provide more information about the notification, Godot signals can also take an argument that provides more information. (For example if the notification was “player took damage” the argument might be an integer that includes how much damage they took.) Like userInfo, this is optional however and you can also fire off a signal with no further information, something like “userUnlockedPro” for when they activate Pro after your SwiftUI paywall. For our simple example, we’re going to send a “selectedIceCream” signal from iOS to Godot, and a “updatedIceCreamCount” signal from Godot to iOS. The former will have a string argument for which ice cream was selected, and the latter will have an integer argument with the updated count. Setting up our Godot project Open Godot.app (available to download from their website) and create a new project, I’ll type in IceCreamParlor, choose the Mobile renderer, then click Create. Godot defaults to a 3D scene, so I’ll switch to 2D at the top, and then in the left sidebar click 2D Scene to create that as our root node. I’ll right-click the sidebar to add a child node, and select Label. We’ll set the text to the “Ice cream:”. In the right sidebar, we’ll go to Theme Overrides and increase the font size to 80 to make it more visible, and we’ll also rename it in the left sidebar from Label to IceCreamLabel. We’ll also do the same to add a Button to the scene, which we’ll call UpdateButton and sets its text to “Update Ice Cream Count”. If you click the Play button in the top right corner of Godot, it will run and you can click the button, but as of now it doesn’t do anything. We’ll select our root node (Node2D) in the sidebar, right click, and select “Attach Script”. Leave everything as default, and click Create. This will now present us with an area where we can actually code in GDScript, and we can refer to the objects in our scene by prefixing their name with a dollar sign. Inside our script, we’ll implement the _ready function, which is essentially Godot’s equivalent of viewDidLoad, and inside we’ll connect to our simple signal we discussed earlier. We’ll do this by grabbing a reference to our singleton, then reference the signal we want, then connect to it by passing a function we want to be called when the signal is received. And of course the function takes a String as a parameter because our signal includes what ice cream was selected. extends Node2D var ice_cream: Array[String] = [] func _ready() -> void: var singleton = Engine.get_singleton("GodotSwiftMessenger") singleton.ice_cream_selected.connect(_on_ice_cream_selected_signal_received) func _on_ice_cream_selected_signal_received(new_ice_cream: String) -> void: # We received a signal! Probably should do something… pass Note that we haven’t actually created the singleton yet, but we will shortly. Also note that normally in Godot, you have to declare custom signals like the ones we’re using, but we’re going to declare them in Swift. As long as they’re declared somewhere, Godot is happy! Let’s also hook up our button by going back to our scene, selecting our button in the canvas, selecting the “Node” tab in the right sidebar, and double-clicking the pressed() option. We can then select that same Node2D script and name the function _on_update_button_pressed to add a function that executes when the button is pressed (fun fact: the button being pressed event is also powered by signals). func _on_update_button_pressed() -> void: pass Setting up our iOS/Swift project Let’s jump over to Xcode and create a new SwiftUI project there as well, also calling it IceCreamParlor. We’ll start by adding the Swift package for SwiftGodotKit to Swift Package Manager, add -lc++ to our “Other Linker Flags” under “Build Settings”, add MetalFX, then go to ContentView.swift and add import SwiftGodotKit at the top. From here, let’s create a simple SwiftUI view so we can choose from some ice cream options. var body: some View { HStack { Button { } label: { Text("Chocolate") } Button { } label: { Text("Strawberry") } Button { } label: { Text("Vanilla") } } .buttonStyle(.bordered) } We’ll also create a new file in Xcode called GodotSwiftMessenger.swift. This will be where we implement our singleton that is akin to NotificationCenter. import SwiftGodot @Godot class GodotSwiftMessenger: Object { public static let shared = GodotSwiftMessenger() @Signal var iceCreamSelected: SignalWithArguments<String> @Signal var iceCreamCountUpdated: SignalWithArguments<Int> } We first import SwiftGodot (minus the Kit), essentially because this part is purely about interfacing with Godot through Godot, and doesn’t care about whether or not it’s embedded in an iOS app. For more details on SwiftGodot see its section below. Then, we annotate our class with the @Godot Swift Macro, which basically just says “Hey make Godot aware that this class exists”. The class is a subclass of Object as everything in Godot needs to inherit from Object, it’s essentially the parent class of everything. Following that is your bog standard Swift singleton initialization. Then, with another Swift Macro, we annotate a variable we want to be our signal which signifies that it’s a Signal to Godot. You can either specify its type as Signal or SignalWithArguments<T> depending on whether or not the specific signal also sends any data alongside it. We’ll use that “somethingHappened” signal we mentioned early, which includes a string for more details on what happened. Note that we used “ice_cream_selected” in Godot but “iceCreamSelected” in Swift, this is because the underscore convention is used in Godot, and SwiftGodotKit will automatically map the camelCase Swift convention to it. Now we need to tell Godot about this singleton we just made. We want Godot to know about it as soon as possible, otherwise if things aren’t hooked up, Godot might emit a signal that we wouldn’t receive in Swift, or vice-versa. So, we’ll hook it up very early in our app cycle. In SwiftUI, you might do this in the init of your main App struct as I’ll show below, and in UIKit in applicationDidFinishLaunching. @main struct IceCreamParlor: App { init() { initHookCb = { level in guard level == .scene else { return } register(type: GodotSwiftMessenger.self) Engine.registerSingleton(name: "GodotSwiftMessenger", instance: GodotSwiftMessenger.shared) } } var body: some Scene { WindowGroup { ContentView() } } } In addition to the boilerplate code Xcode gives us, we’ve added an extra step to the initializer, where we set a callback on initHookCb. This is just a callback that fires as Godot is setup, and it specifies what level of setup has occurred. We want to wait until the level setup is reached, which means the game is ready to go (you could set it up at an even earlier level if you see that as beneficial). Then, we just tell Godot about this type by calling register, and then we register the singleton itself with a name we want it to be accessible under. Again, we want to do this early, as if Godot was already setup in our app, and then we set initHookCb, its contents would never fire and thus we wouldn’t register anything. But don’t worry, this hook won’t fire until we first initialize our Godot game in iOS ourself, so as long as this code is called before then, we’re golden. Lastly, everything is registered in iOS land, but there’s still nothing that emits or receives signals. Let’s change that by going to ContentView.swift, and change our body to the following: import SwiftUI import SwiftGodotKit import SwiftGodot struct ContentView: View { @State var totalIceCream = 0 @State var godotApp: GodotApp = GodotApp(packFile: "main.pck") var body: some View { VStack { GodotAppView() .environment(\.godotApp, godotApp) Text("Total Ice Cream: \(totalIceCream)") HStack { Button { GodotSwiftMessenger.shared.iceCreamSelected.emit("chocolate") } label: { Text("Chocolate") } Button { GodotSwiftMessenger.shared.iceCreamSelected.emit("strawberry") } label: { Text("Strawberry") } Button { GodotSwiftMessenger.shared.iceCreamSelected.emit("vanilla") } label: { Text("Vanilla") } } .buttonStyle(.bordered) } .onAppear { GodotSwiftMessenger.shared.iceCreamCountUpdated.connect { newTotalIceCream in totalIceCream = newTotalIceCream } } } } There’s quite a bit going on here, but let’s break it down because it’s really quite simple. We have two new state variables, the first is to keep track of the new ice cream count. Could we just do this ourselves purely in SwiftUI? Totally, but for fun we’re going to be totally relying on Godot to keep us updated there, and we’ll just reflect that in SwiftUI to show the communication. Secondly and more importantly, we need to declare a variable for our actual game file so we can embed it. We do this embedding at the top of the VStack by creating a GodotAppView, a handy SwiftUI view we can now leverage, and we do so by just setting its environment variable to the game we just declared. Then, we change our buttons to actually emit the selections via signals, and when the view appears, we make sure we connect to the signal that keeps us updated on the count so we can reflect that in the UI. Note that we don’t also connect to the iceCreamSelected signal, because we don’t care to receive it in SwiftUI, we’re just firing that one off for Godot to handle. Communicating Let’s update our gdscript in Godot to take advantage of these changes. func _on_ice_cream_selected_signal_received(new_ice_cream: String) -> void: ice_cream.append(new_ice_cream) $IceCreamLabel.text = "Ice creams: " + ", ".join(ice_cream) func _on_update_button_pressed() -> void: var singleton = Engine.get_singleton("GodotSwiftMessenger") singleton.ice_cream_count_updated.emit(ice_cream.size()) Not too bad! We now receive the signal from SwiftUI and update our UI and internal state in Godot accordingly, as well as the UI by making our ice cream into a comma separated list. And then when the user taps the update button, we then send (emit) that signal back to SwiftUI with the updated count. Running To actually see this live, first make sure you have an actual iOS device plugged in. Unfortunately Godot doesn’t work with the iOS simulator. Secondly, in Godot, select the Project menu bar item, then Export, then click the Add button and select “iOS”. This will bring you to a screen with a bunch of options, but my understanding is that this is 99% if you’re building your app entirely in Godot, you can plug in all the things you’d otherwise plug into Xcode here instead, and Godot will handle them for you. That doesn’t apply to us, we’re going to do all that normally in Xcode anyway, we just want the game files, so ignore all that and select “Export PCK/ZIP…” at the bottom. It’ll ask you where you want to save it, and I just keep it in the Godot project directory, make sure “Godot Project Pack (*.pck)” is selected in the dropdown, and then save it as main.pck. That’s our “game” bundled up, as meager as it is! We’ll then drop that into Xcode, making sure to add it to our target, then we can run it on the device! Here we’ll see choosing the ice cream flavor at the bottom in SwiftUI beams it into the Godot game that’s just chilling like a SwiftUI view, and then we can tap the update button in Godot land to beam the new count right back to SwiftUI to be displayed. Not exactly a AAA game but enough to show the basics of communication 😄 Look at you go! Take this as a leaping off point for all the cool SwiftUI and Godot interoperability that you can accomplish, be it tappings a Settings icon in Godot to bring up a beautifully designed, native SwiftUI settings screen, or confirmation to you your game when the user updated to the Pro version of your game through your SwiftUI paywall. Bonus: SwiftGodot (minus the “Kit”) An additional fun option (that sits at the heart of SwiftGodotKit) is SwiftGodot, which allows you to actually build your entire Godot game with Swift as the programming language if you so choose. Swift for iOS apps, Swift on the server, Swift for game dev. Swift truly is everywhere. For me, I’m liking playing around in GDScript, which is Godot’s native programming language, but it’s a really cool option to know about. Embed size A fear might be that embedding Godot into your app might bloat the binary and result in an enormous app download size. Godot is very lightweight, adding it to your codebase adds a relatively meager (at least by 2025 standards) 30MB to your binary size. That’s a lot larger than SpriteKit’s 0MB, but for all the benefits Godot offers that’s a pretty compelling trade. (30MB was measured by handy blog sponsor, Emerge Tools.) Tips Logging If you log something in Godot/GDScript via print("something") that will also print to the Xcode console, handy! Quickly embedding the pck into iOS Exporting the pck file from Godot to Xcode is quite a few clicks, so if you’re doing it a lot it would be nice to speed that up. We can use the command line to make this a lot nicer. Godot.app also has a headless mode you can use by going inside the .app file, then Contents > MacOS > Godot. But typing the full path to that binary is no fun, so let’s symlink the binary to /usr/local/bin. sudo ln -s "/Applications/Godot.app/Contents/MacOS/Godot" /usr/local/bin/godot Now we can simply type godot anywhere in the Terminal to either open the Godot app, or we can use godot --headless for some command line goodness. My favorite way to do this, is to do something like the following within your Godot project directory: godot --headless --export-pack "iOS" /path/to/xcodeproject/target/main.pck This will handily export the pck and add it to our Xcode project, overwriting any existing pck file, from which point we can simply compile our iOS app. Wrapping it up I really think Godot’s new interoperability with iOS is an incredibly exciting avenue for building games on iOS, be it a full fledged game or a small little easter egg integrated into an existing iOS app, and hats off to all the folks who did the hard work getting it working. Hopefully this serves as an easy way to get things up and running! It might seem like a lot at first glance, but most of the code shown above is just boilerplate to get an example Godot and iOS project up and running, the actual work to embed a game and communicate across them is so delightfully simple! (Also big shout out to Chris Backas and Miguel de Icaza for help getting this tutorial off the ground.)

2 months ago 39 votes
Curing Mac mini M4 fomo with 3D printing

Spoiler: 3D printed! The colored ports really sell the effect If you’re anything like me, you’ve found the new, tinier Mac mini to be absolutely adorable. But you might also be like me that you either already have an awesome M1 Mac mini that you have no real reason to replace, or the new Mac mini just isn’t something you totally need. While that logic might be sound, but it doesn’t make you want one any less. To help cure this FOMO, I made a cute little 3D printable Mac mini that can sit on your desk and be all cute. But then I had an even better idea, the new Mac mini is powerful sure, but it can’t hold snacks. Or a plant. Or your phone. Or pens/pencils. So I also made some versions you can print that add some cute utility to your desk in the form of the new Mac mini. They’re free of course! Just chuck ’em into your (or your friend’s) 3D printer. It even has all the little details modeled, like the power button, ports (including rear), and fan holes! They’re pretty easy to print, it’s in separate parts for ease of printing the bottom a different color (black) versus the top, then just put a dab of glue (or just use gravity) to keep them together. If you have a multi-color 3D printer, you can color the ports and power LED to make it look extra cool (or just do it after the fact with paint). Here are the different options for your desk! Secret item stash The possibilities for what you can store on your desk are now truly endless. Individually wrapped mints? Key switches? Screws? Paper clips? Rubber bands? Flash drives? Download link: https://makerworld.com/en/models/793456 A very green sorta Mac First carbon neutral Mac is cool and all but what if your Mac mini literally had a plant in it? Every desk needs a cute little plant. Download link: https://makerworld.com/en/models/793464 Phone holder A phone/tablet holder is an essential item on my desk for debugging things, watching a video, or just keeping an eye on an Uber Eats order. Before, guests came over and saw my boring phone stand and judged me, now they come over and think I’m exciting and well-traveled. You can even charge your phone/tablet in portrait mode by pushing the cable through a tunnel made through the Ethernet port that then snakes up to the surface. Download link: https://makerworld.com/en/models/793495 Pen holder The Playdate had the cutest little pen/pencil holder accessory but it unfortunately never shipped and my desk is sad. This will be a nice stand in for your beloved pens, pencils, markers, and Apple Pencils. Download link: https://makerworld.com/en/models/793470 A solid model Or if you just want to stare at it without any frills, you can just print the normal model too! Download link: https://makerworld.com/en/models/793447 Printer recommendation Whenever I post about 3D printing I understandably get a bunch of “Which 3D printer should I buy??” questions. This isn’t sponsored, but I’ve found over the last few years the answer has been pretty easy: something from Bambu Lab. Their printers are somehow super easy to use, well designed, and reasonably priced. Prusas are great too, but I think Bambu is hard to beat for the price. Don’t get an Ender. So if you’re looking for a printer now, Black Friday deals are aplenty so it’s pretty much the best time to pick one up. I’d grab something in their A series if you’re on a budget, or the P1S for a bit more if you can swing it (that’s what I use). https://bambulab.com On the other hand if you just want to print one thing now and again, a lot of local libraries are starting to have 3D printers so that might be worth looking into! And online services exist too (eg: JLCPCB and PCBWay), but if you do it with any regularity a 3D printer is a really fun thing to pick up. Enjoy! ❤️ Learning 3D modeling over the last year has been a ton of fun so I love a good excuse to practice, and shout out to Jerrod Hofferth and his amazing 3D printable Mac mini tower (that you should totally download) for the idea to solve my desire with some 3D printing! Also, the models are almost certainly not accurate down to the micrometer as I don’t actually have one, they’re based off Apple’s measurements as well as measuring screenshots. But it should be close! If you have a multi-color 3D printer, the linked models have the colors built-in for your ready to go, but if you want to print it in single-colors I also made versions available with the top and bottom separate as well as the logo, so you can print them separately in the individual colors then connect them with a touch of super glue or something.

8 months ago 72 votes

More in technology

This inexpensive adapter brings Apple Universal Control to vintage Macs

In the distant past of about two decades ago, one would need to use a KVM (Keyboard, Video, Mouse) switch to control multiple computers with the same mouse and keyboard — and even then, it would take a button press to move from one to the other. Today, Apple’s Universal Control feature lets users seamlessly […] The post This inexpensive adapter brings Apple Universal Control to vintage Macs appeared first on Arduino Blog.

18 hours ago 3 votes
How to run Uptime Kuma in Docker in an IPv6-only environment

I use Uptime Kuma to check the availability of a few services that I run, with the most important one being my blog. It’s really nice. Today I wanted to set it up on a different machine to help troubleshoot and confirm some latency issues that I’ve observed, and for that purpose I picked the cheapest ARM-based Hetzner Cloud VM hosted in Helsinki, Finland. Hetzner provides a public IPv6 address for free, but you have to pay extra for an IPv4 address. I didn’t want to do that out of principle, so I went ahead and copied my Docker Compose definition over to the new server. For some reason, Uptime Kuma would start up on the new IPv6-only VM, but it was unsuccessful in making requests to my services, which support both IPv4 and IPv6. The requests would time out and show up as “Pending” in the UI, and the service logs complained about not being able to deliver e-mails about the failures. I confirmed IPv6 connectivity within the container by running docker exec -it uptime-kuma bash and running a few curl and ping commands with IPv6 flags, had no issues with those. When I added a public IPv4 address to the container, everything started working again. I fixed the issue by explicitly disabling the IPv4 network in the Docker Compose service definition, and that did the trick, Uptime Kuma made successful requests towards my services. It seems that the service defaults to IPv4 due to the internal Docker network giving it an IPv4 network to work with, and that causes issues when your machine doesn’t have any IPv4 network or public IPv4 address associated with it. Here’s an example Docker Compose file: name: uptime-kuma services: uptime-kuma: container_name: uptime-kuma networks: - uptime-kuma ports: - 3001:3001" volumes: - /path/to/your/storage:/app/data image: docker.io/louislam/uptime-kuma restart: always networks: uptime-kuma: enable_ipv6: true enable_ipv4: false That’s it! If you’re interested in different ways to set up IPv6 networking in Docker, check out this overview that I wrote a while ago.

25 minutes ago 1 votes
A real PowerBook: the Macintosh Application Environment on a PA-RISC laptop

I like the Power ISA very much, but there's nothing architecturally obvious to say that the next natural step from the Motorola 68000 family must be to PowerPC. For example, the Palm OS moved from the DragonBall to ARM, and it's not necessarily a well-known fact that the successor to Commodore's 68K Amigas was intended to be based on PA-RISC, Hewlett-Packard's "Precision Architecture" processor family. (That was the Hombre chipset, and prototype chips existed prior to Commodore's demise in 1994, though controversy swirled regarding backwards compatibility.) Sure, Apple and Motorola were two-thirds of the AIM alliance, and there were several PowerPC PowerBooks available when the fall of 1997 rolled around. But what if the next PowerBooks had been based on PA-RISC instead? Well, no need to strain yourself imagining it. Here's nearly as close as you're gonna get. that game we must all try running), analyze its performance and technical underpinnings, and uncover an unusual artifact of its history hidden in the executable. (A shout-out to Paul Weissman, the author and maintainer of the incomparable PA-RISC resource OpenPA.net, who provided helpful insights for this article.) near my childhood hometown, RDI Computer Systems was founded in 1989 as Research, Development and Innovations Incorporated in La Costa, California, a neighbourhood of Carlsbad annexed in 1972 in northern San Diego county. (It is completely unrelated to earlier Carlsbad company RDI Video Systems, short for "Rick Dyer Industries" and the developers of laserdisc games like Dragon's Lair and Space Ace, who folded in 1985 after their expensive Halcyon home console imploded mid-development from the 1983 video game crash.) RDI, like several of its contemporaries, was established to capitalize on Sun Microsystems' attempt to commoditize SPARC and open up the market to other OEMs. While most such vendors like Solbourne Computer heavily invested in the desktop workstation segment, RDI instead went even smaller, producing what would become the first SPARC laptops in the United States. Basically SPARCstation IPC and IPX systems crammed into boxy off-white portable cases, the BriteLite series weighed a bit over 13 pounds and started at $10,000 [$24,600]. They were lauded for their performance and compatibility but not their battery life, and RDI became an early adopter of Sun's lower-power microSPARC for the sleeker, sexier PowerLites, using a more dramatic jet-black case. An 85MHz microSPARC II PowerLite 85 was the machine that computational physicist Tsutomu Shimomura, then at the San Diego Supercomputer Center, used to track down hacker Kevin Mitnick in 1995. RDI's initial success enabled its expansion into a bigger 40,000-square foot industrial park facility at 2300 Faraday Avenue, which apparently still exists today. Unfortunately for the company, however, microSPARC hit a performance wall above 125MHz and Sun abandoned further development in 1994, which RDI management took as an indication they needed to diversify. By then the RISC market had started to flourish with many architectures competing for dominance, and RDI decided to throw in with Hewlett-Packard's PA-RISC which had extant portable systems already from Hitachi and SAIC. Neither of those systems had ever existed in large numbers (and the SAIC Galaxys only in military applications at that), giving RDI a new market opportunity with a respected architecture that was already mobile-capable. Producing a final PowerLite in 1996 with the 170MHz Fujitsu TurboSPARC, RDI expanded the PowerLite case substantially for their next systems, replacing the trackball with a touchpad and adding an icon-based LCD status display but keeping its multiple hard disk bays and port options. In the same way the original BriteLites were SPARCstations in every other respect, the new RDI PA-RISC laptop was an otherwise standard HP Visualize B132L or B160L workstation, just inside a laptop case. in our demonstration of Hyper-G, which I've christened ruby after HP chief architect Ruby B. Lee, a key designer of the PA-RISC architecture and its first single-chip implementation. This time around, however, we'll take a closer look at ruby's hardware as a comparison point since it will inform some of the choices we'll make running the Macintosh Application Environment. So that I can save some typing, I'm going to liberally abbreviate "PrecisionBook" to "PABook" for the remainder of this article (avoiding "PBook" so we don't confuse it with PowerBooks). RDI's estimate. I haven't bothered trying to recell this one, and although the status LCD claims it's fully charged, it currently lasts maybe a minute or so which is enough to ride out a AC voltage drop and not much else. Under that is a small 15-pin connector for the optional external 3.5" floppy drive which I don't have either, and behind the other door is the external micro 50-pin SCSI-2 port. A diagram sheet shows that an IR transceiver was planned to be next to the SCSI port, but I don't see one on mine. I took some grabs of the boot process before starting the operating system using a different video mode that my Inogeni VGA capture box would tolerate (my Hall scan converter didn't like it either), though this turned the LCD off, so we won't be bringing up the operating system in this configuration. There is no separate service processor; this all runs on the main CPU. SAIC Galaxy family, and the last and fastest-clocked of the 32-bit PA-RISC 1.1 chips (though the earlier PA-7150 and PA-7200 with their comparatively massive caches can easily beat the 132MHz part). Being effectively a hopped-up PA-7100LC, it inherits most of the characteristics of the earlier chip including a two-way superscalar design, bi-endian support, two asymmetric ALUs, a slightly gimped FPU (the "coprocessor" in the POST summary) with greater double precision latency, and MAX-1 multimedia SIMD instructions. It also incorporates the GSC bus controller on-board. Where the PA-7300LC exceeds its ancestor is in its faster clock speeds — from 132 to 180MHz versus 60 to 100MHz — on a slightly longer six-stage pipeline instead of five, and much larger 64K/64K L1 caches (versus just 1K of I-cache) that were on-die for the first time. In keeping with its "consumer" roots the PA-7300LC additionally includes an L2 cache controller like the PA-7100LC, but here as shown on-screen the PABook's L2 is a full megabyte, and up to 8MB was supported. This is particularly notable given L2 cache was rarely a feature with PA-RISC — large L1s were more typical — and it would not be seen again on a PA-RISC CPU until the PA-8800 seven years later. Other improvements include a 96-entry unified translation lookaside buffer (versus 64) and a four-entry instruction lookaside buffer (versus one) specifically for instruction addresses, which also supported prefetching. The die was fabbed by HP on a 0.5μm process with 9.2 million transistors, 8 million of which were for the L1 cache which consumed most of its 260 square millimetres. A velociraptor can famously be seen in die photos. And here the PowerBook 3400c has a run for its money. This is the point at which I get conflicted because I'm a big fan of the Power ISA, yet I have a real soft spot for PA-RISC because it was my first job out of college, so this is like trying to pick which of my "children" I like best. Although the 3400c with a 240MHz PowerPC 603e was briefly the "world's fastest laptop," at least according to Apple, on benchmarks this 160MHz PA-7300LC wins handily. The last generation 300MHz 603e got SPEC95 numbers of 7.4/6.1, while the 180MHz PA-7300LC recorded scores of 9.22/9.43, with 9.06/9.35 officially recorded for the 180MHz PABook. If we linearly adjust both figures for clock speed we get 5.92/4.88 versus 8.20/8.38, and even using the lower figures in the PABook's technical manual (7.78/7.39 at 160MHz and 6.49/6.54 at 132MHz) the PABook still triumphs. While this isn't a completely fair fight due to the 603's notoriously underpowered FPU, clock for clock the PA-7300LC could challenge both the Pentium Pro and the piledriver PowerPC 604e; the Alpha 21164 could only beat it by revving to 300MHz. And I say all this as a pro-PowerPC bigot! Processing power isn't everything, of course: the 160MHz PA-7300LC does this with a TDP of 15W, while the 300MHz 603e displaces just four to six watts, and the 240MHz part (fabbed on the same 290nm process) is on the lower end of that range. In real world terms that translated to battery life that was at least twice as long on the 3400c. The PABook normally boots from the drive bay closest to the rear (SCSI ID 0; the others are 1 and 2) and the 4GB drive as shipped is in that position, but it can also boot from an external device (SCSI ID 3 or higher) if necessary. The console path is virtually always GRAPHICS(0) for the on-board Visualize-EG and the keyboard path is likewise PS/2, but this can apply to both an external keyboard or the built-in keyboard, which is internally connected the same way. COnfiguration, with the RDI commands for RDI-specific features like mirroring the LCD, but here we'll be asking for INformation on what's installed. INTERNAL_EG_X800, is directly connected to the GSC, but most of the rear ports are connected to a "Bus Adapter." This is the HP LASI ("LAN SCSI") combo chip updated for the PA-7300LC, implementing an Intel i82C596CA 10Mbit NIC, NCR 53C710 SCSI-2 controller, a 16550 UART for RS-232, a WD16C522-compatible parallel port, PS/2 controllers, floppy drive controller and HP Harmony audio. Not enumerated here, a secondary low-speed bus from the LASI called the PHD bus connects to its 1MB flash boot memory, NVRAM and power supply controller. The LASI only supports one serial port, so a second UART is attached to a "Bus Bridge" to provide the second one. This "Bus Bridge" is Dino, a GSC-to-PCI bridge. mikec, I'd like to ask about your experiences with the hardware: please say hi in the comments or drop me a line at ckaiser at floodgap dawt com. directly on AIX — which also used the same PowerOpen ABI — through a thinner runtime layer called Macintosh Application Services (MAS) exclusively for IBM's operating system. only one Apple computer ever ran AIX. In May 1996 Apple updated MAE to version 3.0 with System 7.5.3. This release added compatibility with HP-UX 10.x and made the CPU emulation even faster, primarily through improved handling of condition codes and floating point instructions. It also touted better application compatibility and faster screen updates for users running MAE over a remote X11 session. MAE 3.0 received four point updates up to 3.0.4, badged as "Version 3.0 (Update 4)," which is the final release and the version we'll use. xwd. The installation process is with a shell script and binary installer, both running from within a CDE shell window. Apple offered a trial version so that people could test their software and unlocking the trial limitations requires a license key which you may or may not be able to find on any Archive on the Internet. I won't show the installation process here since it's not particularly interesting or customizeable, but ideally it should be installed to /opt/apple, and even though this version of MAE includes it we're not going to install AppleTalk: ./mae in /opt/apple/bin. The very first run requires us to accept the EULA; there is a specific binary for this and we'll look at it when we take the code apart a bit. /opt? Hang on and we'll get to the on-disk representation. /opt. Again, explanations presently. ruby:/home/spectre/% ls System Folder bin src uploads ruby:/home/spectre/% ls -l System\ Folder/ total 1650 drwxr-xr-x 2 spectre users 1024 Jul 23 21:23 Apple Menu Items -rw-r--r-- 1 spectre users 2846 Jul 23 21:23 Clipboard drwxr-xr-x 2 spectre users 1024 Jul 23 21:23 Control Panels drwxr-xr-x 2 spectre users 1024 Jul 23 21:23 Control Strip Modules drwxr-xr-x 3 spectre users 1024 Jul 23 21:23 Extensions -rw-r--r-- 1 spectre users 35921 Jul 23 21:23 Finder drwxr-xr-x 2 spectre users 1024 Jul 23 21:23 Fonts -rw-r--r-- 1 spectre users 152162 Jul 23 21:23 MAE Enabler -rw-rw-r-- 1 spectre users 18952 Jul 23 21:24 MacTCP DNR drwxr-xr-x 3 spectre users 1024 Jul 23 23:04 Preferences -rw-r--r-- 1 spectre users 72200 Jul 23 21:23 Scrapbook File drwxrwxr-x 2 spectre users 96 Jul 23 21:24 Shutdown Items drwxrwxr-x 2 spectre users 96 Jul 23 21:24 Startup Items -rw-r--r-- 1 spectre users 553762 Jul 23 23:59 System CODE resources) on a native HP-UX Veritas filesystem? The answer is that these files are all AppleSingle, which is to say with their resource and data forks combined, and MAE reads and writes AppleSingle on the fly. There is another interesting folder that gets created. This directory is effectively where the virtual Mac lives. It contains the contents of the virtual Mac's "PRAM" (sm.vpram) plus various databases for files and aliases. The numbered directories require specific explanation. Since each Macintosh volume is its own root, which is certainly not the case in Unix, this directory collects the virtual Mac's volumes here. These aren't symbolic links elsewhere in the filesystem; these are MIVs, or MAE Independent Volumes. They correlate with all the mount points in /etc/fstab by default but any directory can be designated as an MIV, "mounted," and then treated as a volume. We only saw two of them on the desktop because only two of them are "mounted" in /opt/apple/lib/Default_MIV_file, and only those two "volumes" have desktop databases. The home directory is obvious, but /opt was also given a mount because we're running MAE from it and there are various resources in /opt/apple/lib it will try to access. (Some of these are global resources and are treated as part of the System Folder, such as fonts, additional standard applications for the Apple menu, keymaps, locales and, of course, the license key.) These MIVs can be renamed and otherwise treated as if they were any other mounted Macintosh fixed volume. Two other hidden files are also present in this directory, .fs_cache and .fs_info, which maintain the virtual Mac's file and volume information respectively. .fs_cache in particular is very important as it is roughly the global equivalent of an HFS catalog file (and, like a real HFS catalog file, is stored on disk as a B-tree), storing similar metadata like type and creator, timestamps and so forth. This file is so important to MAE that Apple distributed a separate tool called fstool to validate and repair it, sort of like MAE's own Disk First Aid from the shell prompt. You'll have also noticed above that the desktop database in spectre and opt is made up of four files. Desktop DB and Desktop DF are present as usual for the bundle database and Finder information respectively, but there are also two more files %Desktop DB and %Desktop DF, named exactly the same except for a percent sign sigil. This is the other way that resource forks can be represented in MAE, as AppleDouble. Here, the data fork and resource fork are split, with the percent sign indicating the resource fork. Let's explore the MAE System 7.5.3 some more before we attempt to install anything. /opt as they appear in the Finder. /opt is read-only to my uid, so I can't write directly to it. If I had permissions, I could change them from the Permissions dialogue, which is MAE's equivalent of chown, chmod and chgrp all in one. You can also view the (composite) System Folder here and see that it looks pretty much like any other System Folder on any other Mac with the exception of the MAE Enabler. SoftwareFPU. Since not all 68K Macs have floating point units, applications are supposed to use Apple's SANE IEEE-754 library which computes the result in software if no FPU is available. Not all software does this, of course (the Magic Cap 1.0 simulator comes to mind), and this is particularly relevant with Power Macs because the 68K emulator only provides a virtual 68LC040. SoftwareFPU, then, is very simple conceptually: it traps F-line instructions intended for the non-existent coprocessor and turns them into SANE calls. This is slow but it means certain software is able to run that otherwise could not. The MAE SoftwareFPU, which Apple licensed from John Neil & Associates and modified for MAE 3.0, goes a bit further. This version implements a fast path where 68K floating point instructions are directly forwarded to MAE, effectively making F-line traps into hypercalls. Apple estimated this was about 50 percent faster than using regular SoftwareFPU. That said, you'll notice that SoftwareFPU is disabled, which is the default. We'll come back to this when we benchmark the emulator. In MAE 3.0, SANE was changed to directly use host FPU instructions (either SPARC or PA-RISC) for the most commonly performed floating point operations. This works for single and double precision and ran substantially faster than MAE 2.0, but it doesn't work for the 68K's 80-bit extended-precision type, where double precision operations are performed instead and converted (but with a corresponding loss of precision). The previous behaviour, where SANE is simply run under emulation, can be restored with the -sane command line option. A better solution on Power Macs is Tom Pittman's PowerFPU, which (where possible) uses PowerPC floating point instructions directly rather than SANE. All Power Macs have an FPU, so this works on all Power Macs, and is over ten times faster than SoftwareFPU. xclock or xcal. Frodo Commodore 64 emulator, which I installed in /opt. The terminal window opens, which is important to capture any standard error or output, but otherwise it runs normally outside of MAE. That makes you can use the MAE Finder as ... your desktop. You could make MAE take up the entire screen by passing /opt/apple/bin/mae the appropriate -geometry option and setting the X resource Vuewm*mae*clientDecoration to none, effectively making it rootless, and Apple fully supported and documented doing so. Now you've got a virtual Mac that will launch your native X11 applications as well. Who needs CDE when you've got this? We'll look at another standard control panel that MAE uses for a different purpose in a little while. Meantime, having made a basic survey of the emulator, it's now time to actually run software on it. A benchmark would be a good first test but to do that we need to actually put software on it. thule, my little 128MB Macintosh IIci running NetBSD and Netatalk for AppleShare. I have lots of basic software on here including useful INITs and CDEVs and essential tools like StuffIt Expander. It still runs NetBSD 1.5.2 because I had trouble getting regular AppleTalk DDP working with 1.6 and up, so it's a fun time capsule too. But we don't have AppleTalk in MAE, so how are we going to get files from it? Easy: we're going to download the files from thule with FTP and put them into my home directory while MAE is running. The Finder will see the new files and incorporate them. What about the resource forks? The fact that the files are being served by Netatalk from a non-HFS volume (i.e., BSD FFS) actually makes that easier. Netatalk natively stores anything with a resource fork as AppleDouble, depositing the resource fork itself as a separate file into a hidden directory .AppleDouble. We pull down both the data fork and the resource fork, rename the resource fork with a %, and move them both at the same time into my home directory. On the next Finder update, it sees the "whole" file and it becomes accessible. Mac and moved StuffIt Expander there. We can now work with StuffIt archives and only have to download one file, which saves having to get the resource fork separately. An alternative approach, especially if you are transferring a file directly from an HFS or HFS+ volume, is to turn it into AppleSingle first and copy that over; MAE will use the file as-is. Apple provided a tool for this in later versions of Mac OS X/macOS, though 10.4 and prior, arguably where it would have been most useful because those versions still support the Classic Environment, don't seem to have it. The best alternative there is /Developer/Tools/SplitForks, which doesn't do AppleSingle but does create separate AppleDouble data and resource fork files, so at least you can copy those. We'll get to a somewhat more automatic way of specifically handling Netatalk's AppleDouble directories a bit later. over 23 years ago and here we are. I wrote SieveAhl in Modula-2 using the unfortunately named MacMETH compiler just to be weird, rolling all the Toolbox calls by hand. It implements the Sieve of Eratosthenes and a modified version of the FPU-dependent Ahl's Simple Benchmark and issues a score relative to my Macintosh Plus which I have as a reference standard. The main advantage SieveAhl has over other benchmarks is that I wrote it intentionally to run on just about any Mac, even down to System 1.1 (tested in vMac). Here, I'm simply grabbing the StuffIt archive using Internet Explorer 5 for UNIX on the CDE side and saving it into the Mac folder. .sit files isn't too swift on other 68K systems either. We now have a Mac directory that looks like this from the Unix side: Our newly created files in the SieveAhl Folder are now AppleSingle, for example the readme file: We'll get to the rules about when MAE creates AppleSingle and AppleDouble files in a moment. Let's see the numbers we get. Byte in September 1981. It iterates over the interval 0-8190, in which 1,899 primes are expected. Creative Computing, intended originally to evaluate performance and precision differences between various microcomputer BASIC implementations. We don't care about the accuracy or randomness values his benchmark would compute (well, we don't care much), so we just compute those and throw them away. This gets 4,863% the speed of a Mac Plus, which we would expect to be roughly the same because we have no floating point hardware. Repeated runs of both tests were nearly identical. regular SoftwareFPU, not against using it at all. Additionally, MacMETH generates well-behaved code that calls SANE as it should and doesn't emit floating point instructions. How does this compare to a real 68LC040? Conveniently, we have one handy to try it out on! rintintin, my PowerBook 540c with a 33MHz 68LC040 and 12MB of RAM running Mac OS 7.6.1, and the most powerful Blackbird PowerBook sold in the United States (the later Japanese 550c is the same speed, but with a full 68040 and FPU). It was the first PowerBook with any '040 processor, stereo speakers, on-board Ethernet (via AAUI), a trackpad instead of a trackball, twin battery bays and a full-size keyboard. The PowerBook 520/520c and 540/540c came out just a couple months after the first Power Macs and Apple placed the processor onto a daughtercard as a promise that it could be eventually upgraded. As such, the "Ready for PowerPC upgrade" sticker came on these models from the factory, though this particular one is a slightly larger reproduction I printed up a few copies of so I could surreptitiously slap them on the Intel Macs at the Apple Store. Apple nevertheless greatly underestimated demand for the line, mistakenly believing people would rather wait for what eventually was the PowerBook 5300, and the Blackbirds were chronically short-stocked for months. I upgraded this particular unit with an additional 8MB of RAM (on top of the base 4MB) and a SCSI2SD, making it an almost silent unit in operation. The only flaw it has is an iffy cable connection between the display and the top case, which is unfortunately a common problem with these models. Mission: Impossible movie with Tom Cruise and Jon Voight. A regular Blackbird in the standard two-tone grey, most likely a 540c, was what Luther used to block the NOC list transmission on the TGV in the third act. any Blackbird laptop anymore by the time the movie came out, neither computer's model badge is ever visible, though you can at least see a rainbow apple on Luther's. MacLynx, the venerable text browser natively ported to the MacOS. Here we'll run beta 6. lynx.cfg to point to our local Crypto Ancienne TLS 1.3 proxy server. However, we don't have a proper text editor installed other than SimpleText. We could certainly grab BBEdit Lite from the server as well, but MAE gives us an alternative. The manual indicates that "[b]y default, MAE stores files (except text files) in AppleSingle format." Text files, however, are stored as AppleDouble. If we look at our directory listing after unStuffing MacLynx, we can see this rule has been followed: You'll notice that all the text files — the readmes, index.html, lynx.cfg and lynxrc — got separate resource forks as AppleDouble, but the main executable did not. Now, the smart ones among you will say, "But wait! The SieveAhl readme file is text, and it was AppleSingle!" That's right — except that file's text is all stored in styled text resources and there's nothing in the data fork at all. MAE seems to content-sniff files to figure out what to do with them, so BinHex files (which are valid text) will be treated as a text file and made AppleDouble, but a read-only SimpleText file with nothing in the data fork will be treated as a binary and made AppleSingle. The AppleDouble control panel allows you to always force storing files as AppleDouble with specific applications and the separately distributed asdtool will convert a Mac file between AppleSingle and AppleDouble from the shell prompt. vi or, for the mentally deranged, emacs — and the resource fork will remain undisturbed. With MacLynx thus configured for the proxy server, we can view modern HTTPS sites inside MAE no problem. move it. That almost sounds like that the MAE desktop doesn't belong to any MIV even though it is, in fact, part of the MIV for your home directory and that's where we had StuffIt Expander: Conveniently, if you open an alias to something on an MIV that's defined but not yet mounted, MAE will automount it on the desktop for you. Our next set of programs will be TattleTech 2.8 and Gestalt.Appl so we can see what's going on under the hood. There are some surprises here. _L at the end of the Device sResource Name is significant because /opt/apple/bin/macd defines six such resources: Display_Video_Apple_MAE_S, Display_Video_Apple_MAE_M, Display_Video_Apple_MAE_L, Display_Video_Apple_MAE_F, Display_Video_Apple_MAE_C1 and Display_Video_Apple_MAE_C2. These apparently correlate to specific resolutions, namely (in the order they appear) "512 x 342 (9" Macintosh)", "640 x 480 (14" Macintosh)", "832 x 624 (17" Macintosh)", "864 x 864 Resolution", "640 x 800 Resolution" and "640 x 640 Resolution". Since we're at 832x624, we get the _L (presumably small, medium, large, full and two custom?) "card." The emulated DeclROM used by these virtual cards is part of the big blob stored in /opt/apple/lib/engine, along with the Toolbox ROM and other goodies. We'll come back to this when we explore its Gestalt selectors. are defined, but neither appears to be supported. MAE naturally supports printing, but only to a "UNIX PostScript printer" (i.e., lpr) or via AppleTalk, and it does not support using the modem port for a modem or even as a serial port. deprecated it for 68K in 1996. allow the Apple Network Server to numbercrunch for connected clients. It should be possible to do something similar with MAE and have the local host do the work, but there are no local AppleTalk interfaces or headers to compile against. QuickDraw is naturally present (no GX or 3D, of course). In MAE 3.0 QuickDraw is especially important and we'll get to that when we try playing a couple rather famous games. QuickTime is also present, version 2.5, though not everything is enabled (no software MIDI synthesizer, for example). -noextensions. cith selector ("Sith"? Darth Mac?), which is unique to MAE and is conveniently set to $00000304 (i.e., major version 3, minor version 4). While I don't have MAE 1.0 here to test with, René Ros indicates in his Gestalt reports that it has a cith value of 0, though it does exist there too. I don't know what version MAE 2.0 reports but I'm sure someone is firing it up right now to find out and will post in the comments. To more easily decipher the others we'll turn to Gestalt.Appl and I'll point out the highlights. micn is not interesting for the icon (just generic Mac) but rather the string shown, which is a STR# resource indexed by the value of mach (-16395). The string is "Macintosh ApplicationEnvironment" [sic]. mmu " (note space), but this isn't a surprise for an emulator. Consequently, there is also no virtual memory support within MAE (the host is supposed to handle that). romv) is more interesting. Although a great many Old World Mac ROMs are tagged as version 1917, the particular ROM that MAE is using is from the Quadra 660AV and 840AV because we can find its checksum (5b f1 0f d1) and version (07 7d) at offset $001c0000 in /opt/apple/lib/engine. No other valid checksum and version appears anywhere else in this file, and no true Scotsman Macintosh LC would have used a ROM that recent. Thus, if you get a Gestalt ID of 19 but a ROM version of 1917, that's a pretty good indication you're running under MAE. René's list also shows a ROM version of 1917 for MAE 1.0, so MAE 2.0 almost certainly does as well. sltc insists there aren't any NuBus slots. snhw for the sound hardware, which reports a driver cith. This likewise appears nowhere else and is specific to the MAE emulated audio hardware. Oddly, although MAE 1.0 lacked sound, René's list indicates an snhw of awac which would suggest an AWACS. Let's get back to running some more apps. I'm going to bump up the emulated RAM now because a couple near the end will likely benefit. still sold! — was a bit of a mixed bag on MAE. 2.1.2 was the 68K version I had on thule, so I tried that first. It starts and runs fine, but when I actually tried to download anything with this version of Fetch it locked up the entire emulated Mac as soon the file was transferred. That said, I'm not sure if this is a fault of MAE or Fetch because at the exact same point of the exact same file with the exact same version of Fetch on the 540c, it abruptly dropped the connection and threw an error message — though I note transfer speeds were faster on MAE, probably because of the better hardware, right up until it hit the wall. Fortunately the later Fetch 3.0.1 behaves correctly and Apple even offered that specific version for download from the MAE website. There are specific advantages to using Fetch in MAE because of its transparent support for AppleSingle transfers. Still, grabbing files with MacLynx works fine too (hurray for eating your own dogfood), so I'll mostly use that. ssheven, which works great on my real Macs, crashes on MAE and I'm not sure why. ssheven, though I'd have to get a better debugger up on it to figure it out. ssh, and that would even be more useful. Instead, we should try something really important next. Apple IIgs versions are derived from the Mac version. This port was released in 1994 and the "Accelerated for Power Macintosh" and "System 7 Savvy" stickers give the rough timeframe. It requires a 25MHz 68030 or better and is a fat binary. Given that the PABook doesn't have a floppy drive, I copied over this version by simply Stuffing the installed folder on my Power Macintosh 7300 and downloading it over shell FTP (NCSA Telnet on the Mac contains a very basic FTP server which is handy for this). Wolfenzoom (Gopher link) that will scale-blit the 320x200 to 640x400, and I used to use it on my unaccelerated desktop Macintosh IIci when I first bought this game. However, since I'm all about pushing my luck, let's try the highest resolution. unchecked, the game will try to draw directly to video memory. This doesn't always work, and as you can see in this screenshot, not all of the screen was updating properly in this mode. This observation will become relevant when we try running our next game. You do see where this is going, don't you? does use QuickDraw, and appears correctly, but ... thule, but copying Word 5.1 over was a much bigger situation than simply grabbing a single file and its resource fork; we had a number of double-forked files we needed to move en masse. This Perl script, which works on both Perl 5 and 4.036, will iterate over a copy and move Netatalk's AppleDouble resource files into the proper location for MAE, resolving ambiguities in filenames if needed. Call it with find [directory] -name '.AppleDouble' -print | perl ad2mae to run. Only run this on a copy! When everything was in the right place, I moved the directory into my Mac folder and it was ready to go. ad2mae from the MAE Finder, the Finder determined it was a text file and opened it up in vi in a CDE window ready for editing. Not bad! As MAE 3.0 specifically advertised that it was faster than previous versions over remote X, let's test how well that works from my POWER9 Raptor Talos II in Fedora 42. Being the Wayland refusenik I am, I still run KDE Plasma in X11, so with xhost set appropriately and AllowByteSwappedClients enabled (because the POWER9 is running Power ISA little-endian and the PABook is running big) we should be able to connect: actual Command and Option keys, though (I use a white A1048 USB Apple Keyboard with the Quad G5 and the Talos II). makes it tick. DLOG modal when MAE is formatting the TIV. DLOGs for the MAE Toolbar help we saw earlier. DLOG is one of the modals for the floppy/CD mounter. DITL looks like it's part of a credits easter egg, though I haven't figured out yet how to trigger it. I'm assuming the dog is named Rosco ("In Memory of Rosco - 10/5/96"). Also note the dog's Dr Seuss hat, a callback to MAE's "Cat-in-the-Hat" codename. Here's the MAE 3.0 development team: Peter Blas, Michael Brenner, Matthew Caprile, Mary Chan, Bill Convis, Jerry Cottingham, Ivan Drucker, Gerri Eaton, Tim Gilman, Gary Giusti, Mark Gorlinsky, John Grabowski, Cindi Hollister, Richard W. Johnson, John Kullmann, Tom Molnar, John Morley, Stephen Nelson, Michael Press, Jeff Roberts, Shinji Sato, Marc Sinykin, Earl Wallace, Gayle Wiesner. This list of credits will show up again later. PICTs. I'm not sure what they refer to. Notice that the Toolbar Menu when you use the third mouse button is actually just a bunch of PICT resources. There are some other interesting things of note when we start going through the binaries. I separately extracted the files from the installer packages (they're just cpio archives) to preserve their time stamps for analysis. Let's look at everything that's there and then dig into the most notable individual files. All of the core binaries have a modification date of January 23, 1997, presumably the RTM date. Of the library files, data and engine are probably the most notable. We will look at those seprately. KeymapDepotDB is where the default keymaps MAE uses are kept, and MajorUpdate is instructions to the installer script for how to perform an upgrade to a new major release. Since there's no MAE 4.0, this presumably will never be used again. The manual does not document what btree does, and it has only a single readable string in it: Copyright 1991 Apple Computer, Inc. All Rights Reserved. Ricardo Batista The rest are character set mappings and the EULAs in graphic form for both the MAE demo and the full version (with X bitmap buttons for accept/don't accept in all languages except English): After installation Default_MIV_file also lives in /opt/apple/lib, and optionally AliasList for default aliases to appear when starting MAE. To reduce the size of individual users' System Folders, a substantial portion of the composite MAE System Folder is pulled from the shared directory, and other pieces from /opt/apple/lib/data. /opt/apple/lib/data contains the rest of the System Folder, with common pieces like the System 7.5 jigsaw puzzle (licensed by Apple from Captain's Software), note pad (Light Software), scrapbook, menu bar clock (from Steve Christensen's SuperClock!), desktop patterns, compressed System resources and standard INITs and CDEVs. /opt/apple/sys, however, which we won't do much more with here, is the master template for creating each user's own System Folder. We don't need to look at it again because we already saw my own copy of it. /opt/apple/lib/engine is a mashup of many miscellaneous tools. There are various conglomated binaries in it ratted out by the presence of .text, .data and .bss, plus the fake DeclROM for the virtual video card and the Quadra 660AV/840AV Toolbox ROM it uses. There are also many other interesting strings, and being a 10MB file, there are a lot of them: /dev/null 2>&1 Move failed: (%d) [...] cGetDevPixmap, could not emulate Macintosh color table (%d), exiting. cGetDevPixmap, unknown depth %d in encountered. doVideo, error installing video driver, exiting. doVideo, error initializing NewGDevice, exiting. doVideo, could not emulate any Macintosh video devices. Insufficient shared memory or swap space. Using malloc instead. Performance could be increased by adding more swap space and/or configuring more shared memory into the kernel. ERROR: MAE could not allocate the video screen (%dx%d, %dk). Please increase swap space or kill other processes before restarting MAE. Could not malloc new screen buffer, restoring previous size. Not enough shared memory, using malloc instead. Performance would be increased by configuring more shared memory into the kernel. [...] BUGS on MacPlus/SE, NuMc on later [...] Got the OKAY to clear %s (0x%02x bytes at pram 0x%02x) - [...] QDtoGC: (penMode & kHilitePenModeMask); *punting* QDtoGC: penmode=invert (but not well matched); *punting* [...] Aae: AAAAARGH! Fatal X Error [...] Copyright (c) 1987 Apple Computer, Inc., 1985 Adobe Systems Incorporated, 1983-87 AT&T-IS, 1985-87 Motorola Inc., 1980-87 Sun Microsystems Inc., 1980-87 The Regents of the University of California, 1985-87 Unisoft Corporation, All Rights Reserved. [...] 36 41 2 1 c #FFFFFFFFFFFF . c #000000000000 ... .... ..... ..... ..... ..... .... .. ...... ...... .......... .......... ....................... ......................... ....................... ....................... ....................... ...................... ...................... ...................... ...................... ....................... ...................... ....................... ......................... ....................... ....................... ..................... .................... ................... ........ ........ .... .... 36 41 9 1 c #FFFFFFFFFFFF c #0000BBBB0000 c #FFFFFFFF0000 c #FFFF66663333 c #FFFF64640202 c #DDDD00000000 c #999900006666 c #999900009999 c #00000000DDDD ... .... ..... ..... ..... ..... .... .. ...... ...... .......... .......... ....................... ........................ XXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXX oOOOOOOOOOOOOOOOOooooo oOOOOOOOOOOOOOOOOOOOOo oOOOOOOOOOOOOOOOOOOOOo oOOOOOOOOOOOOOOOOOOOOOo +++++++++++++++++++++++ ++++++++++++++++++++++ +++++++++++++++++++++++ ++@+@+@+@+@+@+@+@+@+@+@+ @@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@# $$$$$$$$$$$$$$$$$$$$ $$$$$$$$$$$$$$$$$$ $$$$$$$$ $$$$$$$ $$$$ $$$$ # CREATOR: MAE 3.0 %d %d %3d %3d %3d GIF87a $Id: ximage_high.c,v 3.6 1996/09/11 08:19:50 johng Exp $ $Id: ximage_icon.c,v 3.0 1995/03/23 20:51:23 cvs Exp $ X36 41 2 1 c #FFFFFFFFFFFF . c #000000000000 ... .... ..... ..... ..... ..... .... .. ...... ...... .......... .......... ....................... ......................... ....................... ....................... ....................... ...................... ...................... ...................... ...................... ....................... ...................... ....................... ......................... ....................... ....................... ..................... .................... ................... ........ ........ .... .... 36 41 9 1 c #FFFFFFFFFFFF c #0000BBBB0000 c #FFFFFFFF0000 c #FFFF66663333 c #FFFF64640202 c #DDDD00000000 c #999900006666 c #999900009999 c #00000000DDDD ... .... ..... ..... ..... ..... .... .. ...... ...... .......... .......... ....................... ........................ XXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXX oOOOOOOOOOOOOOOOOooooo oOOOOOOOOOOOOOOOOOOOOo oOOOOOOOOOOOOOOOOOOOOo oOOOOOOOOOOOOOOOOOOOOOo +++++++++++++++++++++++ ++++++++++++++++++++++ +++++++++++++++++++++++ ++@+@+@+@+@+@+@+@+@+@+@+ @@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@# $$$$$$$$$$$$$$$$$$$$ $$$$$$$$$$$$$$$$$$ $$$$$$$$ $$$$$$$ $$$$ $$$$ PaintIconIntoImage, unknown message type %d. %d %d %d Unable to get Icon window id from MACD, exiting. The SuperROM SuperTeam: Central: Ricardo Batista, Rich Biasi, Philip Nguyen, Roger Mann Kurt Clark, Chas Spillar, Paul Wolf, Clinton Bauder Giovanni Agnoli and Debbie Lockett RISC: Scott Boyd, Tim Nichols and Steve Smith MSAD: Jeff Miller and Fred Monroe Cyclone: Tony Leung, Greg Schroeder, Mark Law, Fernando Urbina Dan Hitchens, Jeff Boone, Craig Prouse, Eric Behnke Mike Bell, Mike Puckett, William Sheet, Robert Polic and Kevin Williams Thankzzz to all who contributed to past ROMs...and System 7.x legal, which is the program that requires you to accept the EULA on first run. legal in particular looks like it's just an embedded Tcl/Tk script. It's also notable that appleping, appletalk, atlookup, asdtool and legal still have symbol tables. The AppleTalk tools in particular list functions related to DDP, LAP, NBP and RTMP, but you'd expect that (legal has symbols for Tcl/Tk instead). Interestingly, a few of the strings in appletalk suggest that LocalTalk might have been, or at least was considered for being, supported at one time. Although mae is the binary that you run directly, macd is what handles a lot of the background stuff and mae communicates with it over IPC. The administrator's manual is (probably intentionally) vague about its exact functions, saying only that it "is a daemon that runs whenever apple/bin/mae runs and helps MAE interact with the UNIX environment. It also cleans up after mae if the mae process is killed." We can get a little better idea of what it does from its own set of strings at least: And now mae itself. Some of these strings and components are duplicative of what we saw in /opt/apple/lib/engine. I'm not sure why they're being used twice. Then we start getting into an unusual section. ] [-o >outfile>] [-step] [-remote_debug] decimalinetport hexinetaddr <A/UX COFF file> [<arg1>] ... [<argn>] emulator: cannot uname(2) local system errno = %d TBATDEBUG SOFTMAC_RESTARTING_NOW TBWARN Midnight Emulator version %s Remote Debug version built %s at %s, loading %s [...] Midnight Debugger Unless otherwise specified, the following rules apply: - Commands are single-letter and case matters a whole lot! - Whitespace between the command and arguments are allowed. - Values are hex by default. - Addresses are automatically forced to be on even address boundaries. - '<68kaddr>' is an address which will be offsetted automatically by the emulator so it lies within the 68k image. For example, on this HP system a <68kaddr> of 0x4600 is a real address of 0x%x. - '<emuaddr>' is an address which is not mucked with in any manner. It can be any address within the emulator address space. [...] HUGGERZ What About Bob? ___ ____ ___ ____( \ .-' `-. / )____ (____ \_____ / (O O) \ _____/ ____) (____ `-----( ) )-----' ____) (____ _____________\ .____. /_____________ ____) (______/ `-.____.-' \______) *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug**Hug**Hug* *Hug* *Hug* *Hug* *Hug**Hug**Hug* *Hug* *Hug* *Hug* *Hug**Hug* *Hug* *T3W* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* *Hug* Here goes a big hug from the MAE Team!!!!! [...] mae binary has a second executable binary in it, called the Midnight Emulator. And we can run it! ] [-o >outfile>] [-step] [-remote_debug] decimalinetport hexinetaddr <A/UX COFF file> [<arg1>] ... [<argn>] ruby:/opt/apple/bin/% ./midnight -h Midnight Emulator version 12:02:00 Remote Debug version built Mar 25 1997 at 10:26:25, loading -h must set LM_LICENSE_FILE env var for midnight ruby:/opt/apple/bin/% setenv LM_LICENSE_FILE /opt/apple/XXXXXXX ruby:/opt/apple/bin/% ./midnight -h Midnight Emulator version 12:02:00 Remote Debug version built Mar 25 1997 at 10:26:25, loading -h Unable to open file -h spindler, my clock-chipped Quadra 800. We'll fire it up in A/UX 3.1. spindler:/bin/% uname -a A/UX spindler 3.1 SVR2 mc68040 spindler:/bin/% file sync sync: COFF object paged executable spindler:/bin/% ls -l sync -rwxr-xr-x 1 bin bin 764 Feb 4 1994 sync spindler:/bin/% dis sync **** DISASSEMBLER **** disassembly for sync section .text $ a8: 23c0 0040 0150 mov.l %d0,0x400150.l $ ae: 518f subq.l &8,%sp $ b0: 2eaf 0008 mov.l 0x8(%sp),(%sp) $ b4: 41ef 000c lea 0xc(%sp),%a0 [...] $ 144: 480e ffff fffc link.l %fp,&-4 $ 14a: 4e71 nop $ 14c: 4e5e unlk %fp $ 14e: 4e75 rts /bin/sync looks like a very small, yet valid A/UX binary we can pull over and see if Midnight will run it. First, a quick negative control by running it on itself: The complaint that it's neither COFF nor engine suggests that its normal state is to be running /opt/apple/lib/engine, though this isn't too interesting, since we would assume MAE does that ordinarily. Regardless, it doesn't like our real A/UX COFF binary, even though it does try to load it. It is particularly strange that mae has a modification date of January 23, 1997, but the Midnight Emulator claims to have been built on March 25, over two months later. More explorations to come, especially into whether this could help to debug MAE itself. At the end of this extensive strange trip, I found I rather liked the way MAE worked, and the integration features with HP-UX in particular really tempt me to try running it as my primary environment on top of CDE or VUE. Its performance was surprisingly good and I think if I had the choice back in the day between buying new a 3400c or this thing, even as noisy, heavy and costly as it is, I might strongly have considered buying the latter. Also, I bet MAE would run like a bat out of hell on my maximally configured C8000 and some additional explorations of the Macintosh Application Environment, possibly also on one of my SAIC Galaxys with a floppy drive and an earlier version of HP-UX, might be the subject of a future article. The MAE team clearly didn't think 3.0 was the end of MAE; in the MAE 3.0 white paper's "Future Directions" they indicate that support for additional hardware platforms and "additional UNIX systems" are "being considered." They acknowledge the fact it doesn't run Power Mac software, even saying that "Apple is also investigating the viability of supporting PowerPC Macintosh applications on MAE." However, the document also adds that "Apple wants to ensure that the performance of RISC-on-RISC emulation will meet customer requirements before committing to this development effort." It's not clear if any work on this was ever done. In the wake of Apple's purchase of NeXT in 1997 there was a mention in Informationweek that MAE would be ported to NeXTSTEP as a solution for legacy applications, but this would have been a limiting choice because of the existing PowerPC software that people wanted to run and I don't think it was ever a truly serious proposal. Although I couldn't find anything obvious in the trade rags about exactly when MAE was cancelled, I suspect Gil Amelio did it at Steve Jobs' suggestion after the buyout, like what happened with the Apple Network Server and OpenDoc. After all, MAE had just become superfluous after Apple adopted Rhapsody as the future Mac OS: now that Apple had its own Unix, there was no reason to support anyone else's. Nevertheless, although the Classic Environment in PowerPC versions of Rhapsody and Mac OS X (nicknamed the "Blue Box") is not a direct descendant of MAE, it's very possible that MAE informed its design. In practice, Classic is actually closer to MAS in concept in that it runs native PowerPC code directly in the so-called "problem state" on a paravirtualized Mac OS 8 or 9, using the standard ROM 68K emulator for 68K applications. Classic is less flexible than MAE in that only one instance can be running on any one Power Mac and only one user can be running it, but it was never going to be the future anyway.

2 days ago 6 votes
High quality, low filesize GIFs

While the GIF format is a little on the older side, it’s still a really handy format in 2025 for sharing short clips where an actual video file might have some compatibility issues. For instance, I find when you just want a short little video on your website, a GIF is still so handy versus a video, where some browsers will refuse to autoplay them, or seem like they’ll autoplay them fine until Low Battery Mode is activated, etc. With GIFs it’s just… easy, and sometimes easy is nice. They’re super handy for showing a screen recording of a cool feature in your app, for instance. What’s not nice is the size of GIFs. They have a reputation of being absolutely enormous from a filesize perspective, and they often are, but that doesn’t have to be the case, you can be smart about your GIF and optimize its size substantially. Over the years I’ve tried lots of little apps that promise to help to no avail, so I’ve developed a little script to make this easier that I thought might be helpful to share. Naive approach Let’s show where GIFs get that bad reputation so we can have a baseline. We’ll use trusty ol’ ffmpeg (in the age of LLMs it is a super handy utility), which if you don’t have already you can install via brew install ffmpeg. It’s a handy (and in my opinion downright essential) tool for doing just about anything with video. For a video we’ll use this cute video of some kittens I took at our local animal shelter: It’s 4K, 30 FPS, 5 seconds long, and thanks to its H265/HEVC video encoding it’s only 19.5 MB. Not bad! Let’s just chuck it into ffmpeg and tell it to output a GIF and see how it does. ffmpeg -i kitties.mp4 kitties.gif Okay, let that run and- oh no. For your sake I’m not even going to attach the GIF here in case folks are on mobile data, but the resulting file is 409.4MB. Almost half a gigabyte for a 5 second GIF of kittens. We gotta do better. Better We can do better. Let’s throw a bunch of confusing parameters at ffmpeg (that I’ll break down) to make this a bit more manageable. ffmpeg -i kitties.mp4 -filter_complex "fps=24,scale=iw*sar:ih,scale=1000:-1,split[a][b];[a]palettegen[p];[b][p]paletteuse=dither=floyd_steinberg" kitties2.gif Okay, lot going on here, let’s break it down. fps=24: we’re dropping down to 24 fps from 30 fps, many folks upload full YouTube videos at this framerate so it’s more than acceptable for a GIF. scale=iw*sar:ih: sometimes video files have weird situations where the aspect ratio of each pixel isn’t square, which GIFs don’t like, so this is just a correction step so that doesn’t potentially trip us up scale=1000:-1: we don’t need our GIF to be 4K, and I’ve found 1,000 pixels across to be a great middle ground for GIFs. The -1 at the end just means scale the height to the appropriate value rather than us having to do the math ourselves. The rest is related to the color palette, we’re telling ffmpeg to scan the entire video to build an appropriate color palette up, and to use the Floyd-Steinberg algorithm to do so. I find this algorithm gives us the highest quality output (which is also handy for compressing it more in further steps) This gives us a dang good looking GIF that clocks in at about 10% the file size at 45.8MB. Link to GIF in lieu of embedding directly Nice! Even better ffmpeg is great, but where it’s geared toward videos it doesn’t do every GIF optimization imaginable. You could stop where we are and be happy, but if you want to shave off a few more megabytes, we can leverage gifsicle, a small command line utility that is built around optimizing GIFs. We’ll install gifsicle via brew install gifsicle and throw our GIF into it with the following: gifsicle -O3 --lossy=65 --gamma=1.2 kitties2.gif -o kitties3.gif So what’s going on here? O3 is essentially gifsicle’s most efficient mode, doing fancy things like delta frames so changes between frames are stored rather than each frame separately lossy=65 defines the level of compression, 65 has been a good middle ground for me (200 I believe is the highest compression level) gamma=1.2 is a bit confusing, but essentially the gamma controls how the lossy parameter reacts to (and thus compresses) colors. 1 will allow it to be quite aggressive with colors, while 2.2 (the default) is much less so. Through trial and error I’ve found 1.2 causes nice compression without much of a loss in quality The resulting GIF is now 23.8MB, shaving a nice additional 22MB off, so we’re now at a meagre 5% of our original filesize. That’s a lot closer to the 4K, 20MB input, so for a GIF I’ll call that a win. And for something like a simpler screen recording it’ll be even smaller! Make it easy Rather than having to remember that command or come back here and copy paste it all the time, add the following to your ~/.zshrc (or create it if you don’t have one already): gifify() { # Defaults local lossy=65 fps=24 width=1000 gamma=1.2 while [[ $# -gt 0 ]]; do case "$1" in --lossy) lossy="$2"; shift 2 ;; --fps) fps="$2"; shift 2 ;; --width) width="$2"; shift 2 ;; --gamma) gamma="$2"; shift 2 ;; --help|-h) echo "Usage: gifify [--lossy N] [--fps N] [--width N] [--gamma VAL] <input video> <output.gif>" echo "Defaults: --lossy 65 --fps 24 --width 1000 --gamma 1.2" return 0 ;; --) shift; break ;; --*) echo "Unknown option: $1" >&2; return 2 ;; *) break ;; esac done if (( $# < 2 )); then echo "Usage: gifify [--lossy N] [--fps N] [--width N] [--gamma VAL] <input video> <output.gif>" >&2 return 2 fi local in="$1" local out="$2" local tmp="$(mktemp -t gifify.XXXXXX).gif" trap 'rm -f "$tmp"' EXIT echo "[gifify] FFmpeg: starting encode → '$in' → temp GIF (fps=${fps}, width=${width})…" if ! ffmpeg -hide_banner -loglevel error -nostats -y -i "$in" \ -filter_complex "fps=${fps},scale=iw*sar:ih,scale=${width}:-1,split[a][b];[a]palettegen[p];[b][p]paletteuse=dither=floyd_steinberg" \ "$tmp" then echo "[gifify] FFmpeg failed." >&2 return 1 fi echo "[gifify] FFmpeg: done. Starting gifsicle (lossy=${lossy}, gamma=${gamma})…" if ! gifsicle -O3 --gamma="$gamma" --lossy="$lossy" "$tmp" -o "$out"; then echo "[gifify] gifsicle failed." >&2 return 1 fi local bytes bytes=$(stat -f%z "$out" 2>/dev/null || stat -c%s "$out" 2>/dev/null || echo "") if [[ -n "$bytes" ]]; then local mb mb=$(LC_ALL=C printf "%.2f" $(( bytes / 1000000.0 ))) echo "[gifify] gifsicle: done. Wrote '$out' (${mb} MB)." else echo "[gifify] gifsicle: done. Wrote '$out'." fi } This will allow you to easily call it as either gifify <input-filename.mp4> <output-gifname.gif> and default to the values above, or if you want to tweak them you can use any optional parameters with gifify --fps 30 --gamma 1.8 --width 600 --lossy 100 <input-filename.mp4> <output-gifname.gif>. For instance: # Using default values we used above gifify cats.mp4 cats.gif # Changing the lossiness and gamma gifify --lossy 30 --gamma 2.2 cats.mp4 cats.gif Much easier. May your GIFs be beautiful and efficient.

3 days ago 4 votes
Your Computer Interviewed Chris Curry (1981)

Chris talks about his work with Clive Sinclair and Acorn Computers with a little BBC Micro.

3 days ago 6 votes