TDC::RestoreDC has a horrible bug, which has lingered since OWL 5, at least. It does not play nice with the "orphan control" performed by TDC. In fact, RestoreDC is just a thin wrapper of the Windows API function RestoreDC. It is completely oblivious to the state tracking done by TDC as part of the orphan control.
This means that any references that may have been acquired by TDC, to the objects deselected by RestoreDC, will not be released, and references to the objects reselected by RestoreDC will not be acquired. There may hence be undetected orphans, while detected orphans will dangle and pile up. The following simple loop crashes in my test at about 10000 iterations:
for (...)
{
const auto stateId = dc.SaveDC();
const auto pen = TPen{...};
dc.SelectObject(pen);
dc.MoveTo(...);
dc.LineTo(...);
dc.RestoreDC(stateId);
}
I find this issue quite puzzling. The pair of functions SaveDC and RestoreDC are very useful in nested graphics routines. Similar functions for saving and restoring system state are often found in other graphics systems (well, that is what I remember from what I was taught in college back around 1990). You would think they were often used. But I cannot find any mention in the discussion forum, nor in the mailing list, about their usage or any problems. No bug tickets. Nothing at all. Strange.
An Internet search found this post from 2001 in a discussion about "Object Destruction in MFC" by Doug Harris, who seems to have caught on to the problem: "OWL reference-counted its C++ GDI objects, which I thought was a fine idea. The only problem I recall (it's been 5 years) was that it didn't do anything special for SaveDC/RestoreDC, and I believe it needed to". Why, Doug, did you not file a bug report with OWLNext? I also found this post from 1998 at ExpertsExchange by Quinlan, who reports a crash issue with a code segment using SaveDC and RestoreDC (the reply/solution is not public, though). I also got many hits about SaveDC and RestoreDC in OWL teaching material, so you would think the functions would be in use.
Now that I have rediscovered this issue, I remember running into it as a young programmer over 25 years ago. This was long before OWLNext and my participation online, so I didn't seek any help. Not having figured out what the issue was, I stopped using SaveDC and RestoreDC altogether, and I haven't used these functions since — until now, as I seek to better understand and improve upon TDC. See [discussion:00f93e1a3a].
Discussion: 00f93e1a3a
Discussion: Selecting and restoring objects in TDC
News: 2026/08/owlet---gdi-overhaul-and-new-shared-pointer-semantics
Wiki: Selecting_and_restoring_objects_in_TDC
Note that calling TDC::RestoreObjects before TDC::RestoreDC will prevent orphans piling up and the program crashing. However, the orphan control is still messed up, since references to the reselected objects are not acquired, meaning a handle may be deleted despite still being selected.
This issue was fixed in Owlet in [r5445] as follows:
Please review. If you can fault my logic, let me know. I would like this fix to be robust, so any feedback would be appreciated.
Related
Commit: [r5445]
Claude Code:
Hi @vattila, I have reviewed the fix in [r5445] and merged it into trunk in [r8975], with some corrections. As you asked for faults in the logic, here they are.
The invariant maintained by
SelectObjectandRestorePenetc. is: TDC holds a reference to the selected object if, and only if,OrgXis set and the selected object is notOrgX. Checked against that, r5445 has three holes:The deselected handle is always released. If
OrgXis null, or the deselected handle isOrgX, TDC never acquired a reference to it. If that original is an OWL object, e.g. aTFontselected by an outer TDC on the same HDC, its owner loses a reference, and the object may be deleted prematurely."A null orgHandle implies that the restored handle is the original value" does not hold. Counterexample:
SelectObject(a); SaveDC(); RestorePen(); RestoreDC(id);. NowOrgPenis null, but the restored pen isa, not the original. No reference toais acquired, soacan be deleted while selected, and sinceOrgPenstays null, the original pen is never restored. In this case, the deselected handle is the original, so it can simply be saved inOrgPen.OrgXis not reset when RestoreDC brings back the original. AfterSaveDC(); SelectObject(a); RestoreDC(id);, the selected object isOrgX, butOrgXis still set. A laterRestoreX, or the destructor, then releases a reference to the original that TDC never acquired. That is harmless for stock objects, but not for OWL objects.Also,
GetCurrentObjectreturns null on a metafile DC (TMetaFileDC), so theOWL_CHECKs fire there.The trunk version, per object type, is:
I tested it with a headless harness (memory DC, v145 x64 debug Unicode):
SaveDClevels: before the fix, the program aborted. After the fix, there are no leaks, and the originals are restored.RestoreFontbetweenSaveDCandRestoreDC: before the fix, the counts were wrong and the original font was not restored. After the fix, the test passes.TMetaFileDCpassed both before and after the fix.Cases 4 and 5 are the ones r5445 gets wrong, by my reading of the code. I did not run it.
There is a remaining limitation, which this fix does not address: the saved DC state holds no references. In
SelectObject(a); SaveDC(); SelectObject(b); RestoreDC(id);, ifa's owner has already been destroyed, the secondSelectObjectreleases the last reference, andais deleted while it is still referenced by the saved state. In my test,DeleteObjectreturns TRUE in that case, but the handle stays valid and is reselected byRestoreDC. That is undocumented GDI behaviour, and the object is presumably leaked. A robust solution would letSaveDCpush references for the selected objects onto a stack, and letRestoreDCpop them. That requires an ABI change, so for now the documentation says that objects selected whenSaveDCis called must outlive the correspondingRestoreDC.Two related issues:
TPrintPreviewDCdoes not overrideRestoreDC, so a restore does not reach the printer DC or resync the preview font.SelectStockObjectdoes not saveOrgXwhen it is null, and releases the original instead.Related
Commit: [r5445]
Commit: [r8975]
Follow-up in [r8976]: the remaining limitation and the two related issues I mentioned are now fixed in trunk.
Saved states hold references. TDC::SaveDC acquires references to the reference-counted objects selected at that time and pushes them on a stack. RestoreDC releases them for the popped states, and the destructor releases any that remain (unbalanced SaveDC). So in SelectObject(a); SaveDC(); SelectObject(b); RestoreDC(id);, a is no longer deleted while the saved state refers to it, even if its owner is destroyed in the meantime. For relative indexes, RestoreDC probes the new depth of the context stack with a ::SaveDC/::RestoreDC(-1) pair, so states saved outside the TDC are accounted for. Only objects that are registered at SaveDC time are held, since an unregistered handle may be adopted and registered later.
TPrintPreviewDC overrides SaveDC/RestoreDC. It saves and restores the printer DC along with the screen DC, using a list of the paired levels, and then calls SyncFont and ReScale.
TDC::SelectStockObject maintains the original objects. If no original is saved, the deselected object is saved as the original (and not released); otherwise only a held reference is released. DEFAULT_PALETTE is now selected with ::SelectPalette, since ::SelectObject does not select palettes.
This adds members to TDC and TPrintPreviewDC, so it is an ABI break.
Tests (headless; v145 x64 debug Unicode), compared with [r8975]:
An object selected only in a saved state, after its owner is destroyed: with r8975, it was deleted (not registered). Now it is alive, and it is released after the restore.
Relative restores across an external ::SaveDC, and an unbalanced SaveDC at destruction: with r8975, there were no saved-state references. Now the counts are correct, and the references are released at destruction.
SelectStockObject with an original font owned by another TDC: with r8975, that font lost a reference, it was not restored, and DEFAULT_PALETTE was not selected. Now the test passes.
TPrintPreviewDC with Microsoft Print to PDF, 200 save/select/restore cycles: with r8975, the printer font and text color were not restored. Now both are restored, the preview font is in sync, and no GDI objects leak.
The earlier [r8975] tests still pass.
Related
Commit: [r8975]
Commit: [r8976]