diff options
Diffstat (limited to '')
-rw-r--r-- | vcl/README | 242 | ||||
-rw-r--r-- | vcl/README.GDIMetaFile | 190 | ||||
-rw-r--r-- | vcl/README.lifecycle | 325 | ||||
-rw-r--r-- | vcl/README.scheduler | 394 | ||||
-rw-r--r-- | vcl/README.vars | 52 |
5 files changed, 1203 insertions, 0 deletions
diff --git a/vcl/README b/vcl/README new file mode 100644 index 000000000..e0944688f --- /dev/null +++ b/vcl/README @@ -0,0 +1,242 @@ +Visual Class Library is responsible for the widgets (windowing, buttons, controls, file-pickers etc.), operating system abstraction, including basic rendering (e.g. the output device). + +It should not be confused with Borland's Visual Component Library, which is entirely unrelated. + +VCL provides a graphical toolkit similar to gtk+, Qt, SWING etc. + +source/ + + the main cross-platform chunk of source + +inc/ + + cross-platform abstraction headers + +headless/ + + a backend renderer that draws to bitmaps + +android/ + + Android backend + +osx/ + + macOS backend + +ios/ + + iOS backend + +quartz/ + + code common to macOS and iOS + +win/ + + Windows backend + +qt5/ + + Qt5 (under construction) + +unx/ + + X11 backend and its sub-platforms + gtk3/ + + GTK3 support + kf5/ + + KF5 support (based on qt5 VCL plugin mentioned above) + gtk3_kde5/ + + GTK3 support with KDE5 file pickers (alternative to native kf5 one) + generic/ + + raw X11 support + + plugadapt/ + + pluggable framework to select correct unx backend + + dtrans/ + + "data transfer" - clipboard handling + + http://stackoverflow.com/questions/3261379/getting-html-source-or-rich-text-from-the-x-clipboard + for tips how to show the current content of the + clipboard + + +How the platform abstraction works + + + InitVCL calls 'CreateSalInstance' + + this is implemented by the compiled-in platform backend + + it stores various bits of global state in the + 'SalData' (inc/saldatabasic.hxx) structure but: + + the SalInstance vtable is the primary outward facing gateway + API for platform backends + + It is a factory for: + SalFrames, SalVirtualDevices, SalPrinters, + Timers, the SolarMutex, Drag&Drop and other + objects, as well as the primary event loop wrapper. + +Note: references to "SV" in the code mean StarView, which was a +portable C++ class library for GUIs, with very old roots, that was +developed by StarDivision. Nowadays it is not used by anything except +LibreOffice (and OpenOffice). + +"svp" stands for "StarView Plugin". + +== COM threading == + +The way COM is used in LO generally: +- vcl puts main thread into Single-threaded Apartment (STA) +- oslWorkerWrapperFunction() puts every thread spawned via oslCreateThread() + into MTA (free-threaded) + +== GDIMetafile == + +GDIMetafile is a vector drawing representation that corresponds directly +to the SVM (StarView Metafile) format; it is extremely important as +an intermediate format in all sorts of drawing and printing operations. + +There is a class MetafileXmlDump in include/vcl/mtfxmldump.hxx that +can store a GDIMetafile as XML, which makes debugging much easier +since you can just use "diff" to see changes. + +== EMF+ == + +emf+ is vector file format used by MSO and is successor of wmf and +emf formats. see +http://msdn.microsoft.com/en-us/library/cc230724.aspx for +documentation. note that we didn't have this documentation from +start, so part of the code predates to the time when we had guessed +some parts and can be enhanced today. there also still many thing not +complete + +emf+ is handled a bit differently compared to original emf/wmf files, +because GDIMetafile is missing features we need (mostly related to +transparency, argb colors, etc.) + +emf/wmf is translated to GDIMetafile in import filter +vcl/source/filter/wmf and so special handling ends here + +emf+ is encapsulated into GDIMetafile inside comment records and +parsed/rendered later, when it reaches cppcanvas. It is parsed and +rendered in cppcanvas/source/mtfrenderer. also note that there are +emf+-only and emf+-dual files. dual files contains both types of +records (emf and emf+) for rendering the images. these can used also +in applications which don't know emf+. in that case we must ignore +emf records and use emf+ for rendering. for more details see +documentation + +parsing: + + wmf/emf filter --> GDI metafile with emf+ in comments --> cppcanvas metafile renderer + +lately the GDIMetafile rendering path changed which also influenced +emf+ rendering. now many things happen in drawing layer, where +GDIMetafile is translated into drawing layer primitives. for +metafiles with emf+ we let the mtfrenderer render them into bitmap +(with transparency) and use this bitmap in drawinlayer. cleaner +solution for current state would be to extend the drawing layer for +missing features and move parsing into drawing layer (might be quite +a lot of work). intermediary enhancement would be to know better the +needed size/resolution of the bitmap, before we render emf+ into +bitmap in drawing layer. Thorsten is working on the same problem with +svg rendering, so hopefully his approach could be extended for emf+ as +well. the places in drawing layer where we use canvas mtfrenderer to +render into bitmaps can be found when you grep for GetUseCanvas. also +look at vcl/source/gdi/gdimetafile.cxx where you can look for +UseCanvas again. moving the parsing into drawinglayer might also have +nice side effect for emf+-dual metafiles. in case the emf+ records +are broken, it would be easier to use the duplicit emf +rendering. fortunately we didn't run into such a broken emf+ file +yet. but there were already few cases where we first though that the +problem might be because of broken emf+ part. so far it always turned +out to be another problem. + +rendering: + + before + + vcl --> cppcanvas metafile renderer --> vcl + + now + + drawing layer --> vcl --> cppcanvas metafile renderer --> vcl --> drawing layer + +another interesting part is actual rendering into canvas bitmap and +using that bitmap later in code using vcl API. + +EMF+ implementation has some extensive logging, best if you do a dbgutil +build, and then + +export SAL_LOG=+INFO.cppcanvas.emf+INFO.vcl.emf + +before running LibreOffice; it will give you lots of useful hints. + +You can also fallback to EMF (from EMF+) rendering via + +export EMF_PLUS_DISABLE=1 + + +== Printing/PDF export == + +Printing from Writer works like this: + +1) individual pages print by passing an appropriate OutputDevice to XRenderable +2) in drawinglayer, a VclMetafileProcessor2D is used to record everything on + the page (because the OutputDevice has been set up to record a GDIMetaFile) +3) the pages' GDIMetaFiles are converted to PDF by the vcl::PDFWriter + in vcl/source/gdi/pdfwriter* + +Creating the ODF thumbnail for the first page works as above except step 3 is: + +3) the GDIMetaFile is replayed to create the thumbnail + +On-screen display differs in step 1 and 2: + +1) the VCL Window gets invalidated somehow and paints itself +2) in drawinglayer, a VclPixelProcessor2D is used to display the content + + +=== Debugging PDF export === + +Debugging the PDF export becomes much easier when +compression is disabled (so the PDF file is directly readable) and +the MARK function puts comments into the PDF file about which method +generated the following PDF content. + +The compression can be disabled even using an env. var: + +export VCL_DEBUG_DISABLE_PDFCOMPRESSION=1 + +To de-compress the contents of a PDF file written by a release build or +other programs, use the "pdfunzip" tool: + +bin/run pdfunzip input.pdf output.pdf + +=== SolarMutexGuard === + +The solar mutex is the "big kernel lock" of LibreOffice, a global one. It's a +recursive mutex, so it's allowed to take the lock on the same thread multiple +times, and only the last unlock will actually release the mutex. + +UNO methods on components can be called from multiple threads, while the +majority of the codebase is not prepared for multi-threading. One way to get +around this mismatch is to create a SolarMutexGuard instance at the start of +each & every UNO method implementation, but only when it is necessary: + +- Only acquire the SolarMutex if you actually need it (e.g., not in functions + that return static information). + +- Only around the code that actually needs it (i.e., never call out with it + locked). + +This way you ensure that code (not prepared for multithreading) is still +executed only on a single thread. + +In case you expect that your caller takes the solar mutex, then you can use +the DBG_TESTSOLARMUTEX() macro to assert that in dbgutil builds. + +Event listeners are a special (but frequent) case of the "never call out with +a mutex (SolarMutex or other) locked" fundamental rule: + +- UNO methods can be called from multiple threads, so most implementations + take the solar mutex as their first action when necessary. + +- This can be problematic if later calling out (an event handler is called), + where the called function may be an UNO method implementation as well and + may be invoked on a different thread. + +- So we try to not own the solar mutex, whenever we call out (invoke event + listeners). + +In short, never hold any mutex unless necessary, especially not when calling +out. diff --git a/vcl/README.GDIMetaFile b/vcl/README.GDIMetaFile new file mode 100644 index 000000000..98be38d08 --- /dev/null +++ b/vcl/README.GDIMetaFile @@ -0,0 +1,190 @@ +GDIMetaFile class +================= + +The GDIMetaFile class reads, writes, manipulates and replays metafiles via the VCL module. + +A typical use case is to initialize a new GDIMetaFile, open the actual stored metafile and +read it in via GDIMetaFile::Read( aIStream ). This reads in the metafile into the GDIMetafile +object - it can read in an old-style VCLMTF metafile (back in the days that Microsoft didn't +document the metafile format this was used), as well as EMF+ files - and adds them to a list +(vector) of MetaActions. You can also populate your own GDIMetaFile via AddAction(), +RemoveAction(), ReplaceAction(), etc. + +Once the GDIMetafile object is read to be used, you can "play" the metafile, pause it, wind +forward or rewind the metafile. The metafile can be moved, scaled, rotated and clipped, as +well have the colours adjusted or replaced, or even made monochrome. + +The GDIMetafile can be used to get an OutputDevice's metafile via the Linker() and Record() +functions. + + +Using GDIMetafile +----------------- + +First, create a new GDIMetafile, this can be done via the default constructor. It can then +be constructed manually, or you can use Record() on an OutputDevice to populate the +GDIMetaFile, or of course you can read it from file with Read(). From here you can then +elect to manipulate the metafile, or play it back to another GDIMetafile or to an +OutputDevice via Play(). To store the file, use Write(). + +CONSTRUCTORS AND DESTRUCTORS + +- GDIMetaFile +- GDIMetaFile( cosnt GDIMetaFile& rMtf ) - copy constructor +- ~GDIMetaFile + + +OPERATORS + +- operator = +- operator == +- operator != + + +RECORDING AND PLAYBACK FUNCTIONS + +- Play(GDIMetaFile&, size_t) - play back metafile into another metafile up + to position +- Play(OutputDevice*, size_t) - play back metafile into OutputDevice up to + position +- Play(OutputDevice*, Point, Size, size_t) - play back metafile into OutputDevice at a + particular location on the OutputDevice, up + to the position in the metafile +- Pause - pauses or continues the playback +- IsPause +- Stop - stop playback fully +- WindStart - windback to start of the metafile +- windPrev - windback one record +- GetActionSize - get the number of records in the metafile + + +METAFILE RECORD FUNCTIONS + +- FirstAction - get the first metafile record +- NextAction - get the next metafile record from the + current position +- GetAction(size_t) - get the metafile record at location in file +- GetCurAction - get the current metafile record +- AddAction(MetaAction*) - appends a metafile record +- AddAction(MetaAction*, size_t) - adds a metafile record to a particular + location in the file +- RemoveAction - removes record at file location +- Clear - first stops if recording, then removes all + metafile records +- push_back - pushes back, basically a thin wrapper to the + metafile record list + + +READ AND WRITING + +- Read +- Write +- GetChecksum +- GetSizeBytes + + +DISPLACEMENT FUNCTIONS + +- Move( long nX, long nX) +- Move( long nX, long nX, long nDPIX, long nDPIY ) - Move method getting specifics how to + handle MapMode( MapUnit::MapPixel ) + + +TRANSFORMATION FUNCTIONS + +- Scale( double fScaleX, double fScaleY ) +- Scale( const Fraction& rScaleX, const Fraction& rScaleY ) +- Mirror +- Rotate( long nAngle10 ) +- Clip( const Rectangle& ) + + +COLOR ADJUSTMENT FUNCTIONS + +- Adjust - change luminance, contrast, gamma and RGB via a + percentage +- Convert - colour conversion +- ReplaceColors +- GetMonochromeMtf + + +Related classes +--------------- + +MetaAction: a base class used by all records. It implements a command-like pattern, and also +acts as a prototype for other actions. + + +CONSTRUCTORS AND DESTRUCTORS + +- MetaAction() - default constructor, sets mnRefCount to 1 and + mnType, in this case MetaActionType::NONE +- MetaAction(sal_uInt16 nType) - virtual constructor, sets mnType to nType, and + mnRefCount to 1 +- ~MetaAction + + +COMMAND FUNCTION + +- Execute(OutputDevice*) - execute the functionality of the record to the + OutputDevice. Part of command pattern. + + +FACTORY FUNCTION + +- Clone() - prototype clone function + + +MANIPULATION FUNCTIONS + +- Move(long nHorzMove, long nVerMove) +- Scale(double fScaleX, double fScaleY) + + +READ AND WRITE FUNCTIONS + +- Read +- Write +- ReadMetaAction - a static function, only used to determine which + MetaAction to call on to read the record, which + means that this is the function that must be used. + + +INTROSPECTIVE FUNCTIONS + +- GetType + + + +A note about MetaCommentAction: +------------------------------- + +So this class is the most interesting - a comment record is what is used to extended metafiles, to +make what we call an "Enhanced Metafile". This basically gets the OutputDevice's connect metafile +and adds the record via this when it runs Execute(). It doesn't actually do anything else, unlike +other MetaActions which invoke functions from OutputDevice. And if there is no connect metafile in +OutputDevice, then it just does nothing at all in Execute. Everything else works as normal (Read, +Write, etc). + + + +Basic pseudocode +---------------- + +The following illustrates an exceptionally basic and incomplete implementation of how to use +GDIMetafile. An example can be found at vcl/workben/mtfdemo.cxx + + +DemoWin::Paint() +{ + // assume that VCL has been initialized and a new application created + + Window* pWin = new WorkWindow(); + GDIMetaFile* pMtf = new GDIMetaFile(); + + SvFileStream aFileStream("example.emf", STEAM_READ); + + ReadWindowMetafile(aFileStream, pMtf); + pMtf->Play(pWin); + +} diff --git a/vcl/README.lifecycle b/vcl/README.lifecycle new file mode 100644 index 000000000..a309b65ef --- /dev/null +++ b/vcl/README.lifecycle @@ -0,0 +1,325 @@ +** Understanding transitional VCL lifecycle ** + +---------- How it used to look ---------- + + All VCL classes were explicitly lifecycle managed; so you would +do: + Dialog aDialog(...); // old - on stack allocation + aDialog.Execute(...); +or: + Dialog *pDialog = new Dialog(...); // old - manual heap allocation + pDialog->Execute(...); + delete pDialog; +or: + std::shared_ptr<Dialog> xDialog(new pDialog()); // old + xDialog->Execute(...); + // depending who shared the ptr this would be freed sometime + + In several cases this lead to rather unpleasant code, when +various shared_ptr wrappers were used, the lifecycle was far less than +obvious. Where controls were wrapped by other ref-counted classes - +such as UNO interfaces, which were also used by native Window +pointers, the lifecycle became extremely opaque. In addition VCL had +significant issues with re-enterancy and event emission - adding +various means such as DogTags to try to detect destruction of a window +between calls: + + ImplDelData aDogTag( this ); // 'orrible old code + Show( true, ShowFlags::NoActivate ); + if( !aDogTag.IsDead() ) // did 'this' go invalid yet ? + Update(); + + Unfortunately use of such protection is/was ad-hoc, and far +from uniform, despite the prevalence of such potential problems. + + When a lifecycle problem was hit, typically it would take the +form of accessing memory that had been freed, and contained garbage due +to lingering pointers to freed objects. + + +---------- Where we are now: ---------- + + To fix this situation we now have a VclPtr - which is a smart + reference-counting pointer (include/vcl/vclptr.hxx) which is + designed to look and behave -very- much like a normal pointer + to reduce code-thrash. VclPtr is used to wrap all OutputDevice + derived classes thus: + + VclPtr<Dialog> pDialog( new Dialog( ... ), SAL_NO_ACQUIRE ); + ... + pDialog.disposeAndClear(); + + However - while the VclPtr reference count controls the + lifecycle of the Dialog object, it is necessary to be able to + break reference count cycles. These are extremely common in + widget hierarchies as each widget holds (smart) pointers to + its parents and also its children. + + Thus - all previous 'delete' calls are replaced with 'dispose' + method calls: + +** What is dispose ? + + Dispose is defined to be a method that releases all references + that an object holds - thus allowing their underlying + resources to be released. However - in this specific case it + also releases all backing graphical resources. In practical + terms, all destructor functionality has been moved into + 'dispose' methods, in order to provide a minimal initial + behavioral change. + + As such a VclPtr can have three states: + + VclPtr<PushButton> pButton; + ... + assert (pButton == nullptr || !pButton); // null + assert (pButton && !pButton->IsDisposed()); // alive + assert (pButton && pButton->IsDisposed()); // disposed + +** ScopedVclPtr - making disposes easier + + While replacing existing code with new, it can be a bit + tiresome to have to manually add 'disposeAndClear()' + calls to VclPtr<> instances. + + Luckily it is easy to avoid that with a ScopedVclPtr which + does this for you when it goes out of scope. + +** One extra gotcha - an initial reference-count of 1 + + In the normal world of love and sanity, eg. creating UNO + objects, the objects start with a ref-count of zero. Thus + the first reference is always taken after construction by + the surrounding smart pointer. + + Unfortunately, the existing VCL code is somewhat tortured, + and does a lot of reference and de-reference action on the + class -during- construction. This forces us to construct with + a reference of 1 - and to hand that into the initial smart + pointer with a SAL_NO_ACQUIRE. + + To make this easier, we have 'Instance' template wrappers + that make this apparently easier, by constructing the + pointer for you. + +** How does my familiar code change ? + + Lets tweak the exemplary code above to fit the new model: + +- Dialog aDialog(... dialog params ... ); +- aDialog.Execute(...); ++ ScopedVclPtrInstance<Dialog> pDialog(... dialog params ... ); ++ pDialog->Execute(...); // VclPtr behaves much like a pointer + +or: +- Dialog *pDialog = new Dialog(... dialog params ...); ++ VclPtrInstance<Dialog> pDialog(... dialog params ...); + pDialog->Execute(...); +- delete pDialog; ++ pDialog.disposeAndClear(); // done manually - replaces a delete +or: +- std::shared_ptr<Dialog> xDialog(new Dialog(...)); ++ ScopedVclPtrInstance<Dialog> xDialog(...); + xDialog->Execute(...); ++ // depending how shared_ptr was shared perhaps ++ // someone else gets a VclPtr to xDialog +or: +- VirtualDevice aDev; ++ ScopedVclPtrInstance<VirtualDevice> pDev; + + Other things that are changed are these: + +- pButton = new PushButton(NULL); ++ pButton = VclPtr<PushButton>::Create(nullptr); +... +- vcl::Window *pWindow = new PushButton(NULL); ++ VclPtr<vcl::Window> pWindow; ++ pWindow.reset(VclPtr<PushButton>::Create(nullptr)); + +** Why are these 'disposeOnce' calls in destructors ? + + This is an interim measure while we are migrating, such that + it is possible to delete an object conventionally and ensure + that its dispose method gets called. In the 'end' we would + instead assert that a Window has been disposed in its + destructor, and elide these calls. + + As the object's vtable is altered as we go down the + destruction process, and we want to call the correct dispose + methods we need this disposeOnce(); call for the interim in + every destructor. This is enforced by a clang plugin. + + The plus side of disposeOnce is that the mechanics behind it + ensure that a dispose() method is only called a single time, + simplifying their implementation. + + +---------- Who owns & disposes what ? ---------- + + Window sub-classes tend to create their widgets in one of two +ways and often both. + + 1. Derive from VclBuilderContainer. The VclBuilder then owns + many of the sub-windows, which are fetched by a 'get' + method into local variables often in constructors eg. + + VclPtr<PushButton> mpButton; // in the class + , get(mpButton, "buttonName") // in the constructor + mpButton.clear(); // in dispose. + + We only clear, not disposeAndClear() in our dispose method + for this case, since the VclBuilder / Container truly owns + this Window, and needs to dispose its hierarchy in the + right order - first children then parents. + + 2. Explicitly allocated Windows. These are often created and + managed by custom widgets: + + VclPtr<ComplexWidget> mpComplex; // in the class + , mpComplex( VclPtr<ComplexWidget>::Create( this ) ) // constructor + mpComplex.disposeAndClear(); // in dispose + + ie. an owner has to dispose things they explicitly allocate. + + In order to ensure that the VclBuilderConstructor + sub-classes have their Windows disposed at the correct time + there is a disposeBuilder(); method - that should be added + -only- to the class immediately deriving from + VclBuilderContainer's dispose. + +---------- What remains to be done ? ---------- + + * Cleanup DogTags + + * Expand the VclPtr pattern to many other less + than safe VCL types. + + * create factory functions for VclPtr<> types and privatize + their constructors. + + * Pass 'const VclPtr<> &' instead of pointers everywhere + + add 'explicit' keywords to VclPtr constructors to + accelerate compilation etc. + + * Cleanup common existing methods such that they continue to + work post-dispose. + + * Dispose functions should be audited to: + + not leave dangling pointsr + + shrink them - some work should incrementally + migrate back to destructors. + + * VclBuilder + + ideally should keep a reference to pointers assigned + in 'get()' calls - to avoid needing explicit 'clear' + code in destructors. + + * VclBuilder 'makeFoo' methods + + these should return VclPtr<> types and have their + signatures adjusted en-masse. + + currently we use a VclPtr<> constructor with + SAL_NO_ACQUIRE inside the builder. + +---------- FAQ / debugging hints ---------- + +** Compile with dbgutil + + This is by far the best way to turn on debugging and + assertions that help you find problems. In particular + there are a few that are really helpful: + + vcl/source/window/window.cxx (Window::dispose) + "Window ( N4sfx27sidebar20SidebarDockingWindowE (Properties)) + ^^^ class name window title ^^^ + with live children destroyed: N4sfx27sidebar6TabBarE () + N4sfx27sidebar4DeckE () 10FixedImage ()" + + You can de-mangle these names if you can't read them thus: + + $ c++filt -t N4sfx27sidebar20SidebarDockingWindowE + sfx2::sidebar::SidebarDockingWindow + + In the above case - it is clear that the children have not been + disposed before their parents. As an aside, having a dispose chain + separate from destructors allows us to emit real type names for + parents here. + + To fix this, we will need to get the dispose ordering right, + occasionally in the conversion we re-ordered destruction, or + omitted a disposeAndClear() in a ::dispose() method. + + => If you see this, check the order of disposeAndClear() in + the sfx2::Sidebar::SidebarDockingWindow::dispose() method + + => also worth git grepping for 'new sfx::sidebar::TabBar' to + see where those children were added. + +** Check what it used to do + + While a ton of effort has been put into ensuring that the new + lifecycle code is the functional equivalent of the old code, + the code was created by humans. If you identify an area where + something asserts or crashes here are a few helpful heuristics: + + * Read the git log -u -- path/to/file.cxx + + => Is the order of destruction different ? + + in the past many things were destructed (in reverse order of + declaration in the class) without explicit code. Some of these + may be important to do explicitly at the end of the destructor. + + eg. having a 'Idle' or 'Timer' as a member, may now need an + explicit .Stop() and/or protection from running on a + disposed Window in its callback. + + => Is it 'clear' not 'disposeAndClear' ? + + sometimes we get this wrong. If the code previously used to + use 'delete pFoo;' it should now read pFoo->disposeAndClear(); + Conversely if it didn't delete it, it should be 'clear()' it + is by far the best to leave disposing to the VclBuilder where + possible. + + In simple cases, if we allocate the widget with VclPtrInstance + or VclPtr<Foo>::Create - then we need to disposeAndClear it too. + +** Event / focus / notification ordering + + In the old world, a large amount of work was done in the + ~Window destructor that is now done in Window::dispose. + + Since those Windows were in the process of being destroyed + themselves, their vtables were adjusted to only invoke Window + methods. In the new world, sub-classed methods such as + PreNotify, GetFocus, LoseFocus and others are invoked all down + the inheritance chain from children to parent, during dispose. + + The easiest way to fix these is to just ensure that these + cleanup methods, especially LoseFocus, continue to work even + on disposed Window sub-class instances. + +** It crashes with some invalid memory... + + Assuming that the invalid memory is a Window sub-class itself, + then almost certainly there is some cockup in the + reference-counting; eg. if you hit an OutputDevice::release + assert on mnRefCount - then almost certainly you have a + Window that has already been destroyed. This can easily + happen via this sort of pattern: + + Dialog *pDlg = VclPtr<Dialog>(nullptr /* parent */); + // by here the pDlg quite probably points to free'd memory... + + It is necessary in these cases to ensure that the *pDlg is + a VclPtr<Dialog> instead. + +** It crashes with some invalid memory #2... + + Often a ::dispose method will free some pImpl member, but + not NULL it; and (cf. above) we can now get various virtual + methods called post-dispose; so: + + a) delete pImpl; pImpl = NULL; // in the destructor + b) if (pImpl && ...) // in the subsequently called method + diff --git a/vcl/README.scheduler b/vcl/README.scheduler new file mode 100644 index 000000000..52c76dac5 --- /dev/null +++ b/vcl/README.scheduler @@ -0,0 +1,394 @@ += Introduction = + +The VCL scheduler handles LOs primary event queue. It is simple by design, +currently just a single-linked list, processed in list-order by priority +using round-robin for reoccurring tasks. + +The scheduler has the following behaviour: + +B.1. Tasks are scheduled just priority based +B.2. Implicitly cooperative AKA non-preemptive +B.3. It's not "fair" in any way (a consequence of B.2) +B.4. Tasks are handled round-robin (per priority) +B.5. Higher priorities have lower values +B.6. A small set of priorities instead of an flexible value AKA int + +There are some consequences due to this design. + +C.1. Higher priority tasks starve lower priority tasks + As long as a higher task is available, lower tasks are never run! + See Anti-pattern. + +C.2. Tasks should be split into sensible blocks + If this can't really be done, process pending tasks by calling + Application::Reschedule(). Or use a thread. + +C.3. This is not an OS scheduler + There is no real way to "fix" B.2. and B.3. + If you need to do a preemptive task, use a thread! + Otherwise make your task suspendable. + + += Driving the scheduler AKA the system timer = + + 1. There is just one system timer, which drives LO event loop + 2. The timer has to run in the main window thread + 3. The scheduler is run with the Solar mutex acquired + 4. The system timer is a single-shot timer + 5. The scheduler system event / message has a low system priority. + All system events should have a higher priority. + +Every time a task is started, the scheduler timer is adjusted. When the timer +fires, it posts an event to the system message queue. If the next most +important task is an Idle (AKA instant, 0ms timeout), the event is pushed to +the back of the queue, so we don't starve system messages, otherwise to the +front. + +Every time the scheduler is invoked it searches for the next task to process, +restarts the timer with the timeout for the next event and then invokes the +task. After invoking the task and if the task is still active, it is pushed +to the end of the queue and the timeout is eventually adjusted. + + += Locking = + +The locking is quite primitive: all interaction with internal Scheduler +structures are locked. This includes the ImplSchedulerContext and the +Task::mpSchedulerData, which is actually a part of the scheduler. +Before invoking the task, we have to release the lock, so others can +Start new Tasks. + + += Lifecycle / thread-safety of Scheduler-based objects = + +A scheduler object it thread-safe in the way, that it can be associated to +any thread and any thread is free to call any functions on it. The owner must +guarantee that the Invoke() function can be called, while the Scheduler object +exists / is not disposed. + + += Anti-pattern: Dependencies via (fine grained) priorities = + +"Idle 1" should run before "Idle 2", therefore give "Idle 1" a higher priority +then "Idle 2". This just works correct for low frequency idles, but otherwise +always breaks! + +If you have some longer work - even if it can be split by into schedulable, +smaller blocks - you normally don't want to schedule it with a non-default +priority, as it starves all lower priority tasks. Even if a block was processed +in "Idle 1", it is scheduled with the same (higher) priority again. Changing +the "Idle" to a "Timer" also won't work, as this breaks the dependency. + +What is needed is task based dependency handling, so if "Task 1" is done, it +has to start "Task 2" and if "Task 1" is started again, it has to stop +"Task 2". This currently has to be done by the implementor, but this feature +can be added to the scheduler reasonably. + + += Implementation details = + +== General: event priority for DoYield == + +There are three types of events, with different priority: + +1. LO user events +2. System events +3. LO Scheduler event + +They should be processed according to the following code: + +bool DoYield( bool bWait, bool bAllCurrent ) +{ + bool bWasEvent = ProcessUserEvents( bAllCurrent ); + if ( !bAllCurrent && bWasEvent ) + return true; + bWasEvent = ProcessSystemEvents( bAllCurrent, &bWasSchedulerEvent ) || bWasEvent; + if ( !bWasSchedulerEvent && IsSchedulerEvent() ) + { + ProcessSchedulerEvent() + bWasEvent = true; + } + if ( !bWasEvent && bWait ) + { + WaitForSystemEvents(); + bWasEvent = true; + } + return bWasEvent; +} + +== General: main thread deferral == + +In almost all VCL backends, we run main thread deferrals by disabling the +SolarMutex using a boolean. In the case of the redirect, this makes +tryToAcquire and doAcquire return true or 1, while a release is ignored. +Also the IsCurrentThread() mutex check function will act accordingly, so all +the DBG_TESTSOLARMUTEX won't fail. + +Since we just disable the locks when we start running the deferred code in the +main thread, we won't let the main thread run into stuff, where it would +normally wait for the SolarMutex. + +Eventually this will move into the SolarMutex. KDE / Qt also does main +thread redirects using Qt::BlockingQueuedConnection. + +== General: processing all current events for DoYield == + +This is easily implemented for all non-priority queue based implementations. +Windows and macOS both have a timestamp attached to their events / messages, +so simply get the current time and just process anything < timestamp. +For the KDE backend this is already the default behaviour - single event +processing isn't even supported. The headless backend accomplishes this by +just processing a copy of the list of current events. + +Problematic in this regard is the Gtk+ backend. g_main_context_iteration +dispatches "only those highest priority event sources". There is no real way +to tell, when these became ready. I've added a workaround idea to the TODO +list. FWIW: Qt runs just a single timer source in the glib main context, +basically the same we're doing with the LO scheduler as a system event. + +The gen X11 backend has some levels of redirection, but needs quite some work +to get this fixed. + +== General: non-main thread yield == + +Yielding from a non-main thread must not wait in the main thread, as this +may block the main thread until some events happen. + +Currently we wait on an extra conditional, which is cleared by the main event +loop. + +== General: invalidation of elapsed timer event messages == + +Since the system timer to run the scheduler is single-shot, there should never +be more than one elapsed timer event in system event queue. When stopping or +restarting the timer, we eventually have to remove the now invalid event from +the queue. + +But for the Windows and macOS backends this may fail as they have delayed +posting of events, so a consecutive remove after a post will actually yield no +remove. On Windows we even get unwanted processing of events outside of the +main event loop, which may call the Scheduler, as timer management is handled +in critical scheduler code. + +To prevent these problems, we don't even try to remove these events, but +invalidate them by versioning the timer events. Timer events with invalid +versions are processed but simply don't run the scheduler. + +== General: track time of long running tasks == + +There is TaskStopwatch class. It'll track the time and report a timeout either +when the tasks time slice is finished or some system event did occur. + +Eventually it will be merged into the main scheduler, so each invoked task can +easily track it's runtime and eventually this can be used to "blame" / find +other long running tasks, so interactivity can be improved. + +There were some questions coming up when implementing it: + +=== Why does the scheduler not detect that we only have idle tasks pending, +and skip the instant timeout? === + +You never know how long a task will run. Currently the scheduler simply asks +each task when it'll be ready to run, until two runnable tasks are found. +Normally this is very quick, as LO has a lot of one-shot instant tasks / Idles +and just a very few long term pending Timers. + +Especially UNO calls add a lot of Idles to the task list, which just need to +be processed in order. + +=== Why not use things like Linux timer wheels? === + +LO has relatively few timers and a lot one-shot Idles. 99% of time the search +for the next task is quick, because there are just ~5 long term timers per +document (cache invalidation, cursor blinking etc.). + +This might become a problem, if you have a lot of open documents, so the long +term timer list increases AKA for highly loaded LOOL instances. + +But the Linux timer wheel mainly relies on the facts that the OS timers are +expected to not expire, as they are use to catch "error" timeouts, which rarely +happen, so this definitely not matches LO's usage. + +=== Not really usable to find misbehaving tasks === + +The TaskStopwatch class is just a little time keeper + detecting of input +events. This is not about misbehaving Tasks, but long running tasks, which +have to yield to the Scheduler, so other Tasks and System events can be +processed. + +There is the TODO to merge the functionality into the Scheduler itself, at +which point we can think about profiling individual Tasks to improve +interactivity. + +== macOS implementation details == + +Generally the Scheduler is handled as expected, except on resize, which is +handled with different runloop-modes in macOS. In case of a resize, the normal +runloop is suspended in sendEvent, so we can't call the scheduler via posted +main loop-events. Instead the scheduler uses the timer again. + +Like the Windows backend, all Cocoa / GUI handling also has to be run in +the main thread. We're emulating Windows out-of-order PeekMessage processing, +via a YieldWakeupEvent and two conditionals. When in a RUNINMAIN call, all +the DBG_TESTSOLARMUTEX calls are disabled, as we can't release the SolarMutex, +but we can prevent running any other SolarMutex based code. Those wakeup +events must be ignored to prevent busy-locks. For more info read the "General: +main thread deferral" section. + +We can neither rely on macOS dispatch_sync code block execution nor the +message handling, as both can't be prioritized or filtered and the first +does also not allow nested execution and is just processed in sequence. + +There is also a workaround for a problem for pushing tasks to an empty queue, +as [NSApp postEvent: ... atStart: NO] doesn't append the event, if the +message queue is empty. + +An additional problem is the filtering of events on Window close. This drops +posted timer events, when a Window is closed resulting in a busy DoYield loop, +so we have to re-post the event, after closing a window. + +== Windows implementation details == + +Posted or sent event messages often trigger processing of WndProc in +PeekMessage, GetMessage or DispatchMessage, independently from the message to +fetch, remove or dispatch ("During this call, the system delivers pending, +nonqueued messages..."). Additionally messages have an inherited priority +based on the function used to generate them. Even if WM_TIMER messages should +have the lowest priority, a manually posted WM_TIMER is processed with the +priority of a PostMessage message. + +So we're giving up on processing all our Scheduler events as a message in the +system message loop. Instead we just indicate a 0ms timer message by setting +the m_bDirectTimeout in the timer object. This timer is always processed, if +the system message wasn't already our timer. As a result we can also skip the +polling. All this is one more reason to drop the single message processing +in favour of always processing all pending (system) events. + +There is another special case, we have to handle: window updates during move +and resize of windows. These system actions run in their own nested message +loop. So we have to completely switch to timers, even for 0ms. But these +posted events prevent any event processing, while we're busy. The only viable +solution seems to be to switch to WM_TIMER based timers, as these generate +messages with the lowest system priority (but they don't allow 0ms timeouts). +So processing slows down during resize and move, but we gain working painting, +even when busy. + +An additional workaround is implemented for the delayed queuing of posted +messages, where PeekMessage in WinSalTimer::Stop() won't be able remove the +just posted timer callback message. See "General: invalidation of elapsed +timer event messages" for the details. + +To run the required GUI code in the main thread without unlocking the +SolarMutex, we "disable" it. For more infos read the "General: main thread +deferral" section. + +== KDE implementation details == + +This implementation also works as intended. But there is a different Yield +handling, because Qts QAbstractEventDispatcher::processEvents will always +process all pending events. + + += TODOs and ideas = + +== Task dependencies AKA children == + +Every task can have a list of children / a child. + + * When a task is stopped, the children are started. + * When a task is started, the children are stopped. + +This should be easy to implement. + +== Per priority time-sorted queues == + +This would result in O(1) scheduler. It was used in the Linux kernel for some +time (search Ingo Molnar's O(1) scheduler). This can be a scheduling +optimization, which would prevent walking longer event list. But probably the +management overhead would be too large, as we have many one-shot events. + +To find the next task the scheduler just walks the (constant) list of priority +queues and schedules the first ready event of any queue. + +The downside of this approach: Insert / Start / Reschedule(for "auto" tasks) +now need O(log(n)) to find the position in the queue of the priority. + +== Always process all (higher priority) pending events == + +Currently Application::Reschedule() processes a single event or "all" events, +with "all" defined as "100 events" in most backends. This already is ignored +by the KDE backend, as Qt defines its QAbstractEventDispatcher::processEvents +processing all pending events (there are ways to skip event classes, but no +easy way to process just a single event). + +Since the Scheduler is always handled by the system message queue, there is +really no more reasoning to stop after 100 events to prevent LO Scheduler +starvation. + +== Drop static inherited or composed Task objects == + +The sequence of destruction of static objects is not defined. So a static Task +can not be guaranteed to happen before the Scheduler. When dynamic unloading +is involved, this becomes an even worse problem. This way we could drop the +mbStatic workaround from the Task class. + +== Run the LO application in its own thread == + +This would probably get rid of most of the macOS and Windows implementation +details / workarounds, but is quite probably a large amount of work. + +Instead of LO running in the main process / thread, we run it in a 2nd thread +and defer al GUI calls to the main thread. This way it'll hopefully not block +and can process system events. + +That's just a theory - it definitely needs more analysis before even attending +an implementation. + +== Re-evaluate the macOS ImplNSAppPostEvent == + +Probably a solution comparable to the Windows backends delayed PostMessage +workaround using a validation timestamp is better then the current peek, +remove, re-postEvent, which has to run in the main thread. + +Originally I didn't evaluate, if the event is actually lost or just delayed. + +== Drop nMaxEvents from Gtk+ based backends == + +gint last_priority = G_MAXINT; +bool bWasEvent = false; +do { + gint max_priority; + g_main_context_acquire( NULL ); + bool bHasPending = g_main_context_prepare( NULL, &max_priority ); + g_main_context_release( NULL ); + if ( bHasPending ) + { + if ( last_priority > max_priority ) + { + bHasPending = g_main_context_iteration( NULL, bWait ); + bWasEvent = bWasEvent || bHasPending; + } + else + bHasPending = false; + } +} +while ( bHasPending ) + +The idea is to use g_main_context_prepare and keep the max_priority as an +indicator. We cannot prevent running newer lower events, but we can prevent +running new higher events, which should be sufficient for most stuff. + +This also touches user event processing, which currently runs as a high +priority idle in the event loop. + +== Drop nMaxEvents from gen (X11) backend == + +A few layers of indirection make this code hard to follow. The SalXLib::Yield +and SalX11Display::Yield architecture makes it impossible to process just the +current events. This really needs a refactoring and rearchitecture step, which +will also affect the Gtk+ and KDE backend for the user event handling. + +== Merge TaskStopwatch functionality into the Scheduler == + +This way it can be easier used to profile Tasks, eventually to improve LO's +interactivity. diff --git a/vcl/README.vars b/vcl/README.vars new file mode 100644 index 000000000..9f5a10a26 --- /dev/null +++ b/vcl/README.vars @@ -0,0 +1,52 @@ +Environment variables in VCL: + +General +------- +SAL_USE_VCLPLUGIN - use a VCL plugin +SAL_RTL_ENABLED - Enable RTL UI +SAL_NO_NWF - disable native widgets +SAL_FORCEDPI - force a specific DPI (gtk3 & qt5/kf5 plugins only) +SAL_FORCE_HC - force high-contrast mode + +SAL_NO_FONT_LOOKUP - disable font search and fallback and always use a hard-coded font name (for some unit tests) + +LO_COLLECT_UIINFO - enable the uitesting logging, value is expected to be a relative file name that +will be used to write the log under instdir/uitest/. + +VCL_DOUBLEBUFFERING_AVOID_PAINT - don't paint the buffer, useful to see where we do direct painting +VCL_DOUBLEBUFFERING_FORCE_ENABLE - enable double buffered painting +VCL_DOUBLEBUFFERING_ENABLE - enable a safe subset of double buffered painting (currently in Writer, not in any other applications) + +VCL_DEBUG_DISABLE_PDFCOMPRESSION - disable compression in the PDF writer + +Gtk+ +---- + +VCL_GTK3_PAINTDEBUG - in debug builds, if set to 1 then holding down shift+0 forces a redraw event, shift+1 repaints everything, and +shift+2 dumps cairo frames to pngs as /tmp/frame<n>.png + +Bitmap +------ +VCL_NO_THREAD_SCALE - disable threaded bitmap scale +VCL_NO_THREAD_IMPORT - disable threaded bitmap import +EMF_PLUS_DISABLE - use EMF rendering and ignore EMF+ specifics + +OpenGL +------ +SAL_FORCEGL - force enable OpenGL +SAL_GL_NO_SWAP - disable buffer swapping if set (should show nothing) +SAL_GL_SLEEP_ON_SWAP - sleep for half a second on each swap-buffers. +SAL_DISABLE_WATCHDOG - don't start the thread that watches for broken GL/Vulkan/OpenCL drivers + +Skia +---- +SAL_DISABLESKIA=1 - force disabled Skia +SAL_ENABLESKIA=1 - enable Skia, unless blacklisted (and if the VCL backend supports Skia) +SAL_FORCESKIA=1 - force using Skia, even if blacklisted +SAL_SKIA=raster|vulkan - select Skia's drawing method, by default Vulkan is used +SAL_DISABLE_SKIA_CACHE=1 - disable caching of complex images + +OpenGL,Skia +----------- +SAL_WITHOUT_WIDGET_CACHE - disable LRU caching of native widget textures +SAL_DISABLE_GLYPH_CACHING - don't render glyphs through OpenGL textures or Skia surfaces |