MacProject Pro 1.5 resource leveler

This page is docs/leveling.md from the repository, published as written.

Spec for Analyze ▸ Level Resources (⌘L), Unlevel Resources and View Leveler Results, read from the original's code and resources. No sample was leveled through the full path, so the code is the main evidence. One sample (Product Design Subproject) carries the result of a leveling substitution, which confirms the stored fields.

Markers:

Segments: CLeveler 0xe2f00–0xe7a50, CLevelerDlg 0x120600–0x122138, CLevelResults 0x122200–0x123510, CXProjectLeveler 0x123600–0x123f14. Flag labels follow MPXKit's decoders (see file-format.md). A flag label's suffix maps to a bit mask: .a = 0x80, .b = 0x40, .c = 0x20, .d = 0x10, .e = 0x08, .f = 0x04, .10 = 0x02, .11 = 0x01. This is checked against +0x112.c = 0x20 in the scheduler notes, and against the code below.

1. Commands and settings

Command Number Enabled when Runs
Level Resources ⌘L 1046 always CLeveler_3e60
Unlevel Resources 1047 dcst +0x23e.b set (the "leveled" flag), or, with Cross-Project Leveling on, a family member qualifies CLeveler_488c
View Leveler Results 1048 always opens window kind 0xb (view 1012, TLevelChangesWindow)

All three are [code]: menu setup at 0x7102c–0x710a0, dispatch at 0x5b040–0x5b180.

Document settings (dcst; Preferences ▸ Resources, view 3403). The labels are [resource] and the masks [code]:

Field Meaning Samples [data]
+0x23d.b (0x40) Level During Calculate off in all 23
+0x23d.d (0x10) Interactive Leveling: ask at each conflict on in all 23
+0x23d.e (0x08) Cross-Project Leveling (project family) off
+0x23f.b (0x40) Allow Changes Only to Task Starting Dates off
+0x23e.b (0x40) the document has been leveled. Set by every applied change; cleared by Unlevel; enables Unlevel set in Product Design Subproject only
+0x23e.f (0x04) guess: the schedule needs recalculating. With it set and Level During Calculate off, leveling recalculates first

The resource fields:

Field Meaning Status
+0x44.40 (0x40) Level: only these resources take part. In every one of 331 sample resources [code] FUN_000e6374; [data]
+0x20 # Available (float)
+0x1c in memory only: the leveler's segment list

The task fields:

Field Meaning Status
+0x100 Leveling Priority. 0 is "Do Not Level"; 100 is "Highest" (Task Info label)
+0xa2 Early Start Type: bit set, below
+0xa4 Actual Start Type: same bits
+0xa5 Actual Finish Type: same bits
+0xe4 the Earliest Start before leveling [code]
+0xe8 the Actual Start before leveling [code]

Start-type bits:

Bit Name
0x01 User set
0x02 Leveler set
0x04 Swept
0x08 Bad
0x10 Constrained

They are [resource]: STR# 11137, a 32-entry table indexed by the value. The table shows "Leveler set" over "User set" over "Swept", with a "Bad " prefix for 0x08, and "Constrained" only when it is the only bit set. In the samples, 13 tasks hold 0x10 (the constrained start milestones) and 606 hold 0 [data].

Allocation fields (iall). They are all serialized, so Unlevel works after the file is saved and reopened:

Field Meaning Status
+0x2e the resource before a substitution [code] FUN_000e72cc; [data]
+0x3a the Number before leveling (float) [code]
+0x4a the % Effort before leveling [code]
+0x4c.a (0x80) the leveler changed the effort [code] CLeveler_0608
+0x4c.b (0x40) the leveler changed the number [code]
+0x4c.c (0x20) the leveler substituted the resource [code]

More on these:

Leveling Priority names (STR# 6289) [resource]: Do Not Level, Lowest, Very Low, Lower, Low, Medium, High, Higher, Very High, Highest. How the numbers 0–100 map to these names: guess.

2. Who takes part

3. Load and conflicts

All times are seconds (the MacDate axis).

4. The main loop (CLeveler_3e60 @ 0xe6d64, FUN_000e64e8 @ 0xe64e8) [code]

  1. If Level During Calculate is off and +0x23e.f is set, calculate the schedule first (CAnalysis_0080).
  2. Find the overall range: the earliest task start to the latest end, over tasks and gathered allocations (FUN_000e6ca0). Process it as one window. On a memory-full failure (−108) the window is halved, up to 5 times, and retried.
  3. Without Interactive Leveling, show a progress window titled "Leveling resources" (STR 15003) with the conflict's date. Cancelling stops.
  4. Gather the load profile for the window (FUN_000e6a10 → FUN_000e6814 over tasks, FUN_000e69bc over resources).
  5. Repeat until there are no conflicts, or Stop:
    1. Take the next conflict (§3).
    2. Build its solutions (§5): at most 3, sorted best first.
    3. Choose one:
      • Not interactive: the first solution, applied automatically.
      • Interactive: the Interactive Leveling™ dialog (view 11000, CLevelerDlg_0d00). It shows the conflict text, up to four solutions as radio buttons each with a before/after picture (TSolutionPict), and the solution text. Its buttons are Make Changes, Ignore Conflict and Stop Leveling.
    4. If there's no solution, or the user chose Ignore Conflict: mark the segment ignored (+0x28). Rescan from its end later (re-gather once the scan passes it).
    5. Otherwise:
      • record the conflict and the solution for the results (FUN_000e4c3c);
      • truncate the profiles at the conflict start (FUN_000e6bc0);
      • apply the solution (§6);
      • recalculate the schedule (CAnalysis_07dc, which honours leveled starts, §7);
      • re-gather the profile from the conflict start to the window end.
  6. Mark the document changed. Without Cross-Project Leveling, run a final calculation and a redraw (CAnalysis_0460).
  7. If any changes were recorded, show the results (CLevelerDlg_0e38).

5. Solutions for one conflict (FUN_000e504c, FUN_000e6010, FUN_000e5e78, FUN_000e5b78) [code]

The conflict context holds:

There are three kinds of solution. Each is a list of changes: per allocation, none, substitute (type 1), move (type 2), or effort or number change (types 3 and 4).

Moves

The leveler enumerates the sets K of contributions that stay where they are.

For a set K, every movable contribution not in K is moved (type 2) to moveDate, computed by FUN_000e5a64 / FUN_000e5958:

The moved task's new Earliest Start = CAnalysis_1314(task, moveDate, flag), which is the scheduler's start_at(W(moveDate)): the next working start on the task's calendar [CLeveler_0568]. flag is set when the task has no predecessor or no successor list (+0xdc / +0xde = −1) and controls whether a zero-duration task stays on the instant.

Effort or number changes

These are only for rate-changeable contributions, so never when Allow Changes Only to Task Starting Dates is on.

FUN_000e51d0(ctx, k) gives k overlapping allocations an equal share:

FUN_000e537c then:

It combines up to depth allocations, where depth comes from n:

n depth
< 8 n − 1
8 4
9–10 3
11–19 2
≥ 20 1

Substitution

Only with Interactive Leveling on, Allow Changes Only to Task Starting Dates off, and more than one resource in the document. One "Substitute for …" solution is added (type 1, FUN_000e4e0c). Choosing it opens Resource Substitution (view 11001): a "Substitute For" resource list and a task list. This is the dialog's fourth radio button (guess).

Ranking

Solutions are kept in a sorted array whose class overrides Compare with CLeveler_1314 @ 0xe4218. Only the best 3 are kept (FUN_000e5650).

Each solution carries a score, filled in by FUN_000e4194 / FUN_000e4070. For each changed allocation, CLeveler_0360 gives f, the new finish of its task after the change, and LF, the task's Latest Finish (+0xb6). The score fields:

Field Meaning
+0x14 total load of the unchanged contributions
+0x18 moved tasks whose priority < maxPriority
+0x1a Σ (f − LF) over changed tasks at maxPriority
+0x1e Σ (f − LF) over all changed tasks
+0x22 Σ max(0, LF − f) at maxPriority, the slack left
+0x26 Σ max(0, LF − f), all
+0x2a the number of changes
+0x2c moved elements carrying mark [2]; exact meaning guess

The order, best first:

  1. Move or combined solutions before a lone substitution (a single type-1 change).
  2. Fewer +0x2c.
  3. A comparison of +0x14 against the other solution and the capacity (SANE branches): prefer the solution that fills capacity exactly. Details: guess.
  4. More low-priority tasks moved (+0x18): lower-priority tasks are moved first.
  5. Less lateness past LF for the highest-priority tasks (+0x1a), then for all tasks (+0x1e).
  6. More slack left for the highest-priority tasks (+0x22), then for all (+0x26).
  7. The +0x14 comparison again, then fewer changes (+0x2a).

So by default the leveler delays the lowest-priority tasks, choosing the moves that push tasks least past their Latest Finish and keep the most slack. Moves are not limited to slack: lateness is only penalised.

6. Applying a change (CLeveler_0608 @ 0xe350c, an iall method) [code]

7. How the scheduler uses a leveled start [code FUN_0004812c @ 0x4812c]

When the Early Start Type has 0x02 (Leveler set), the forward pass pins the task's start to its stored Earliest Start (+0x54). That overrides both the predecessors' bound and the start constraint, like Start On. Otherwise the constraint rules in scheduling.md apply.

"Bad leveler set" (0x08 | 0x02) is presumably shown when a pinned start falls before what the dependencies require. Guess: the code that sets 0x08 wasn't traced.

The Swift scheduler must add this rule. In the samples no task has 0x02, so the 618 of 619 match is unaffected.

8. Unlevel (CLeveler_488c @ 0xe7790, FUN_000e7388, FUN_000e72cc) [code]

For every task that isn't fixed:

If anything changed, recalculate with Level During Calculate temporarily off. In every case, clear dcst +0x23e.b (leveled).

With Cross-Project Leveling, the same is done in each family file (FUN_000e7604, FUN_000e7520).

9. View Leveler Results (CLevelResults, view 1012 TLevelChangesWindow / TLevelChangesView)

A scrolling list of what the last leveling run did. The message templates are [resource]: STR 11000–11016. The joiners are ", " (11001), " and " (11000), "; " (11004) and "." (11002).

Per conflict:

Then its changes, joined by "; ":

Change Template
move "move start of ^4 to ^1" (11006)
effort down "decrease effort from ^0 to ^1 in ^4" (11007)
effort up "increase effort from ^0 to ^1 in task ^4" (11008)
number down "decrease number from ^2 to ^3 in task ^4" (11009)
both 11010–11014
substitution "Substitute for ^0…" / "Substitute For ^0" (11005, 11016)
ellipsis "…" (11015)

The records are added with CDocumentDataManager_28ac (CLeveler_0b8c): kind 2 a planned move, 1 an actual move, 3 an allocation change, plus the date or rate and the resource and task. They appear to live in a document list, dcst +0x158, which Unlevel clears (guess). Whether they are saved: guess, probably not.

10. What the editor needs to decide

  1. Interactive Leveling is on in every sample. So the original's default experience is the per-conflict dialog: Make Changes / Ignore Conflict / Stop Leveling with up to 4 solutions. Implement the dialog, or default to automatic (the first solution) and offer the dialog behind the preference.
  2. Substitution needs the Resource Substitution dialog. It can follow later; the other solutions don't depend on it.
  3. The scheduler must honour Early Start Type 0x02 (§7) before leveling can work. Unlevel needs +0xe4/+0xe8 and the allocation originals; the model should carry them, and the MPRX writer should round-trip them.
  4. The +0x14 tie-break and the x bound on moveDate are only partly decoded (guesses). A port that skips them still follows the main order: low priority first, then least lateness, then most slack, then fewest changes.

Where things are