> ## Content Index
> Fetch the complete content index at: https://blog.tonalphoto.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Phocus 4.1.1 Bug Investigations - Technical Details
- URL: https://blog.tonalphoto.com/phocus-4-1-1-bug-investigation-technical-details/
- Published: 2026-03-23T19:46:34.000Z
- Updated: 2026-08-20T19:00:29.000Z
- Description: Technical investigation of four Phocus 4.1.1 bugs: stack traces, plist monitoring, log analysis, and root cause identification.
- Author: Konrad Michels
- Tags: Bug Reports, Phocus, #hasselblad

## Inside Four Phocus 4.1.1 Bugs: The Technical Investigation

This is the companion post to "Phocus 4.1.1: Four Things That Should Be on the Fix List," which covers the same four bugs in practical, photographer-friendly terms. This post is the engineering version. It covers the investigation methods, evidence, and root cause analysis behind each finding.

If you're a developer, a technically curious photographer, or someone at Hasselblad reading this, this is the detail behind the conclusions.

---

**A note on support:** This post represents my personal exploration and testing, not official technical support or guidance from Hasselblad. If you need assistance with your Hasselblad equipment, please contact Hasselblad directly: [customersupport@hasselblad.com](mailto:customersupport@hasselblad.com) for global support, [support.us@hasselblad.com](mailto:support.us@hasselblad.com) for the Americas, or visit hasselblad.com/support for regional options.

---

All testing was performed on Phocus 4.1.1 running on Apple Silicon (M4 Pro), macOS 26 Tahoe. Tested on both a Mac Mini Pro (48 GB) and MacBook Pro (48 GB).

---

## Bug 1: Import Adjustment Dropdown Ignored

### The claim

Selecting an [HNCS](https://blog.tonalphoto.com/hasselblad-hncs-color-science-explained/) preset (Nature, Portrait, etc.) in the Import dialog's Adjustment dropdown has no effect. Images always import with the default rendering baseline.

### The evidence

**Visual confirmation:** I imported a single .[3FR](https://blog.tonalphoto.com/hasselblad-fff-vs-3fr-why-renaming-doesnt-work-and-why-thats-good/) with Nature selected. Post-import, the Adjustments toolbar showed "3FR" (raw baseline). Manually switching to Nature after import produced a visible [histogram](https://blog.tonalphoto.com/phocus-histogram-vs-capture-one-levels/) shift. Switching to Standard produced a histogram identical to the post-import state. The rendering after import matched Standard, not Nature, regardless of what was selected in the Import dialog.

**Log analysis:** Phocus writes to `~/Library/Application Support/Phocus/Logs/Phocus.log`. During the import, the log recorded:

```
14:30:04.914  Profile .dfR: not found
14:30:04.915  Profile .dfC: not found
14:30:04.915  Profile .dfG: not found
14:30:04.919  [Pyramid] Adding new pyramid task for /Users/.../2025-11-11_01_00706.3FR<Dust_No><Scene_403_273913372>...
14:30:04.919  [Pyramid] Now processing ...
14:30:05.142  ProcessLowResEntry: ... got buffer success
14:30:05.461  ProcessLowResEntry: ... Laplacian success
14:30:05.461  ProcessLowResEntry: ... complete with pyramid 1

```

The log records pyramid generation (thumbnail building), dust profile lookups, and scene parameters. There is no entry for the Adjustment selection. No "Applying Nature preset" or equivalent. The import code path does not appear to read the dropdown value.

**The `<Scene_403_273913372>` tag** in the pyramid task is notable. The `403` is the HNCS scene identifier (Standard). If Nature had been applied, this value would differ. It matches the Standard baseline, confirming the preset was not applied at import time.

### Probable cause

The Import dialog's Adjustment dropdown is wired to the UI but not connected to the import code path. The dropdown state is never read when the import executes.

---

If you'd like to support this documentation project: [☕ Buy me a coffee](https://buymeacoffee.com/tonalphoto?ref=blog.tonalphoto.com) 

---

## Bug 2: Synchronous File I/O on Main Thread Causes UI Hang

### The claim

Phocus performs synchronous filesystem operations on the main thread inside FSEvents callbacks, causing unbounded UI freezes when file I/O is slow.

### The evidence

macOS captured a stackshot during a hang that lasted approximately 10 minutes before I force-quit the application. The stackshot sampled 26 frames at 100ms intervals. The main thread was blocked on the same call in every single sample:

```
Main thread (com.apple.main-thread), all 26 samples:

  NSApplication run
    → CFRunLoop
      → __CFRunLoopDoSource1
        → __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
          → __CFMachPortPerform
            → FSEventsClientProcessMessageCallback
              → FSEventsD2F_server
                → _Xcallback_rpc
                  → implementation_callback_rpc
                    → [Phocus + 1384972]
                      → [Phocus + 1413808]
                        → [Phocus + 1411532]
                          → [Phocus + 1410228]
                            → [Phocus + 1410068]
                              → NSFileManager.attributesOfItem(atPath:)
                                → String.withFileSystemRepresentation
                                  → _FileManagerImpl.attributesOfItem(atPath:)
                                    → listxattr    ← BLOCKED

```

The chain is:

1. **FSEvents delivers a filesystem change notification** to Phocus via a Mach port callback on the main run loop
2. Phocus's internal handler (four frames of Phocus code without symbols) processes the notification
3. The handler calls **`NSFileManager.attributesOfItem(atPath:)`** synchronously
4. `attributesOfItem` calls **`listxattr`** to enumerate extended attributes
5. `listxattr` blocks in the kernel, waiting for the filesystem to respond
6. The main thread cannot return to the run loop until `listxattr` completes

**Key metrics from the hang report:**

- Process: Phocus 4.1.1, PID 64327, arm64
- Memory footprint: 9005.65 MB (not memory-related, 48 GB available)
- Active threads: 60 (only the main thread was blocked)
- Time since fork: 908 seconds (normal session, \~15 minutes)
- Duration unresponsive before sampling: 8 seconds
- Sampling duration: 2.6 seconds (26 samples × 100ms)
- Actual hang duration: \~10 minutes (required force-quit)
- Fan speed: 0 rpm (not thermally throttled)

Researching and writing the blog and page posts I produce takes up a lot of my time and costs me money for software subscriptions, tooling, and other incidentals related to running a business.

Please consider a paid membership to help me keep the lights on and pay the bills - it is only a fraction of the cost of an X2D II and a lens. Click on the button to go to the membership page to see the options available.

[Support the blog. ](https://blog.tonalphoto.com/membership/) 

### The anti-pattern

FSEvents notifications arrive on whatever thread the run loop is servicing. In Phocus's case, this is the main thread. The correct pattern on macOS is:

1. Receive the FSEvents notification
2. Dispatch any file I/O to a background `DispatchQueue`
3. Post results back to the main thread only for UI updates

Apple's documentation explicitly recommends this. `NSFileManager.attributesOfItem(atPath:)` can block for an unbounded duration depending on the underlying filesystem. On local APFS/SSD, latency is sub-millisecond. On any slower medium (external USB, network mount, or a filesystem under kernel contention), the main thread blocks for the full duration of the I/O.

### The fix

Move the `attributesOfItem(atPath:)` call (and any other file I/O in the FSEvents handler) to a background dispatch queue. The existing Phocus code at offsets +1384972 through +1410068 should dispatch to `DispatchQueue.global()` or a dedicated serial queue before performing file attribute reads.

---

Have you seen the guide? I've published Essential Phocus 4.x for Mac - 85 topics across 250 pages covering everything from HNCS color science to HDR workflows. It's the reference manual Hasselblad hasn't updated since 3.8\. It's $49, and updates are included.

[Get it here ](https://payhip.com/b/6pqAt?ref=blog.tonalphoto.com) 

## Bug 3: Tool Visibility Preferences Written from Stale Memory

### The claim

Phocus maintains two separate in-memory representations of tool visibility state. The quit-time preferences save reads from a stale, initialization-time copy rather than the live copy that tracks user changes.

### The investigation

I couldn't reproduce this on demand, so I built automated monitoring. A LaunchAgent watches `~/Library/Preferences/dk.hasselblad.phocus.plist` via FSEvents and captures a timestamped snapshot every time the file is modified. I deployed this on two machines and let it run continuously.

Tool visibility is stored in the plist inside the `LastLayout` key, which is a base64-encoded XML blob. Within that blob, the `ToolsForAdjust` array contains integer identifiers for each visible tool.

### The overnight capture

On the MacBook Pro, I launched Phocus at 20:28, customized tools during the session, and closed the laptop lid at 21:21\. Nine hours later (06:06), I opened the lid and immediately pressed Cmd+Q.

The plist monitor captured two writes within 12 seconds of termination. The second write (06:07:08) was the reversion: the `ToolsForAdjust` array no longer matched my customizations. Tools I had hidden reappeared. Tools I had made visible were written back as hidden.

I correlated the exact sequence using `log show` (macOS unified log):

```
06:06:20  Lid open
06:06:22  Authentication
06:06:55  Cmd+Q keystroke delivered to Phocus
06:06:56  Termination
06:07:08  Plist reversion write

```

Phocus had been idle in memory for 8 hours 46 minutes.

### The controlled experiment

I then ran 6 sequential plist snapshots to isolate the mechanism:

| Snap | Action                                       | ToolsForAdjust count | Result                                    |
| ---- | -------------------------------------------- | -------------------- | ----------------------------------------- |
| 1    | Phocus running (reverted state from morning) | 15                   | Baseline                                  |
| 2    | Cmd+Q, zero tool changes made                | **19**               | Array changed despite no user interaction |
| 3    | Relaunched                                   | 19                   | Unchanged from snap 2                     |
| 4    | Removed 4 tools via UI, Phocus still running | 19                   | Nothing written to disk                   |
| 5    | Cmd+Q                                        | **15**               | Removals correctly reflected              |
| 6    | Relaunched                                   | 15                   | Changes persisted                         |

**Snap 1 → 2 is the critical result.** The user made zero changes to tool visibility. Yet Cmd+Q wrote a `ToolsForAdjust` array with 19 items (was 15 on disk), with different tools in different visibility states. The quit-time save wrote data that matched neither the on-disk state nor the user's current UI state.

**Snap 3 → 4** proves that unchecking a tool in the Adjust panel writes nothing to disk. Not to the `LastLayout` blob, not to the top-level `tool_rem_*` keys, not to `Layouts.xml`, not to `cfprefsd`, not to any file in `~/Library/Application Support/Phocus/`. The change exists only in process memory.

**The `tool_rem_*` keys** (e.g., `tool_rem_3`, `tool_rem_7`, `tool_rem_10`) are byte-for-byte identical across all 6 snapshots. The tool visibility UI does not write to these keys. They appear to be remnants of an older storage mechanism.

### Root cause (behavioral conclusion)

Phocus maintains two separate representations of tool visibility in memory:

1. **A mutable copy** that tracks the user's live changes (what the UI displays)
2. **An immutable copy** that is frozen at initialization time

The quit-time layout save reads from the immutable copy. This explains:

- **Short-cycle quits work:** Both copies are still similar because insufficient time has passed for divergence
- **Long idle fails:** The immutable copy represents initialization state, which increasingly differs from user configuration
- **Zero-interaction writes produce different data (snap 1→2):** The immutable copy may differ from what was on disk because Phocus constructs it independently at launch
- **It's not a "settings not saving" problem:** The save mechanism works correctly. It reads from the wrong source.

### Additional anti-pattern: write-on-quit

Beyond the dual-copy issue, Phocus only writes preferences at application termination. The standard macOS pattern is `NSUserDefaults`, which persists changes within seconds of a `set()` call via the `cfprefsd` daemon. Writing only on quit means any crash, force-quit, or unexpected termination loses all preference changes made during the session. Combined with Bug #2 (UI hangs requiring force-quit), this creates a compounding failure mode.

### The fix

Two changes needed:

1. The quit-time layout save must read from the mutable copy (the one tracking live user changes), not the initialization-time copy
2. Preference writes should happen when changes are made (via `NSUserDefaults` or equivalent), not only at application termination

### Where to look

The code path that writes `ToolsForAdjust` into the `LastLayout` blob at `applicationWillTerminate:`. The function that gathers tool visibility state before serialization is reading from the wrong object.

---

## Bug 4: "Use Last Saved Adjustments" Architecture

### The claim

The "Use Last Saved Adjustments" button operates from a transient in-memory buffer that is destroyed on commit, not from the .phos sidecar. The button name is the opposite of its behavior.

### The architecture

Phocus's adjustment lifecycle has three states:

1. **Uncommitted:** Adjustments exist only in memory. The .phos sidecar has not been updated.
2. **Committed:** Adjustments have been written to the .phos sidecar (by Save Changes or auto-save).
3. **Buffered:** A copy of the last uncommitted adjustments persists in a transient buffer after commit.

The "Use Last Saved Adjustments" button reads from state 3, the transient buffer. This buffer is populated when a commit event fires (auto-save on navigation, or clicking Save Changes). It contains the adjustments that were just committed.

The buffer is destroyed by the next commit event. This means:

- Edit Image A → navigate to B (auto-save fires on A, buffer populated) → click "Use Last Saved Adjustments" on B → A's adjustments apply. **Works.**
- Continue to Image C without saving B → click "Use Last Saved Adjustments" on C → A's adjustments apply. **Works** (buffer survives because B hasn't been committed).
- Click Save Changes on B → buffer destroyed → navigate to C → button grayed out. **Gone permanently.**
- Return to Image A (whose adjustments are fully recorded in its .phos sidecar) → button still grayed out. **Cannot re-populate from sidecar.**

### The naming problem

The button is labeled "Use Last **Saved** Adjustments" but:

- It does NOT read from saved (committed) data in the .phos sidecar
- It ONLY works with the transient buffer created during the save operation
- The act of saving (the next save, not the one that created the buffer) is precisely what destroys the buffer

A more accurate name would be "Paste Last Adjustments" or "Apply Buffered Adjustments."

### Comparison with other RAW processors

Every other major RAW processor implements adjustment transfer as a read from persisted state:

- **Capture One:** Select source image → Copy Adjustments → select target → Apply Adjustments. Works at any time, regardless of save state.
- **Lightroom:** Select source → Copy Settings → select target → Paste Settings. The source image's develop settings are always available.
- **DxO PhotoLab:** Select source → Copy Correction Settings → paste to targets. Reads from the database.

These implementations read from the committed/persisted adjustment data, not from a transient buffer. The adjustment transfer is decoupled from the save lifecycle.

### The fix

"Use Last Saved Adjustments" should read from the .phos sidecar of the previously active image, not from a transient in-memory buffer. If an image has committed adjustments (which it always does after the first auto-save), the button should be able to read and apply them regardless of when the commit occurred.

### Workaround

Use **Modify** instead. Select the source image plus all target images in the browser, click Modify, and check the tool groups you want to transfer. Modify reads from the source image's current state (committed or not) and applies it to all selected targets. The limitation is that you must know all target images upfront.

---

## Sign up for The Tech Behind the Frame - Hasselblad edition

Practical Hasselblad knowledge the official docs don't cover. Workflows, file behaviour, and real-world insights for Phocus and X2D II users.

Subscribe 

Email sent! Check your inbox to complete your signup. 

No spam. Unsubscribe anytime.