art with code

2008-12-31

Lock-free PreDraw, less caching

The RequestPreDraw-method I was calling in the GTK event loop to tell the PreDraw-thread to queue up a new PreDraw ASAP (cancelling the current PreDraw) was doing things in a bit silly fashion. The CancelPreDraw-method was spinning for the current PreDraw to exit, and even if the PreDraw did have a pretty granular cancellation strategy, it meant that it could impose a "finish current directory listing + sorting"-delay on the draw loop. And the worst case for that is well north of half a second.

So the cancel lock had to go.

Now the RequestPreDraw-method is two lines:

/** BLOCKING */
void RequestPreDraw ()
{
// Tell currently running PreDraw to stop.
Renderer.CancelPreDraw();
// Tell PreDrawThread to start a new PreDraw after the current has stopped.
PreDrawRequested = PreDrawInProgress = true;
}

And Renderer.CancelPreDraw simply sets a "cancel when you please"-flag:

public void CancelPreDraw ()
{
PreDrawCancelled = true;
}

As the PreDraw-thread does the PreDraw-calls sequentially it doesn't need internal locking, and we can get by with a couple state bools. Result? Less code, less locking, a lock-free common drawing loop and a stable framerate.

Less caching

Made the caches throw away pretty much everything not needed on this very frame (last 10 frames for the thumbs.) And it brought memory use to ~80 megs in overview, with peaks at around 150 megs when panning the large thumbs of an image directory.

Ditching the filesystem entry cache was a bit nasty, as it causes a lot of visible relayout. A compromise between memory use and model rebuilding would be preferred. One solution might be to trace a zoom path out of the current position and keep the visible set of that cached. Implementation... genZoomLevels current_z 0 |> map preDraw. So that PreDraw doesn't determine the visible set of a single frame, but a set of frames from the current position to the root directory. Or just keep the last visited N directories cached.

2008-12-30

Some OpenGL renderer analysis

This is all hypothetical wanking to see what sort of performance I might expect from an OpenGL renderer for Filezoo. And with that disclaimer out of the way, on with it.

My goal is 60 frames per second. The application side of the drawing takes currently around 10 milliseconds, so the drawing side gets 6 milliseconds, making the effective drawing framerate target around 170 fps.

Fillrate of one gigapixel per second for single texturing sounds like a nice conservative estimate. The drawing area is 400x1000 pixels with maybe 25% overdraw, for a total of 500 kpixels per frame. Multiply by four for 4x FSAA and it's 2 Mpx per frame. At 1000 Mpx / second, 500 fps. Well above the 170 fps target.

For geometry, let's assume 40 million triangles per second. The maximum amount of files drawn by the renderer is currently at around 2000, each requiring four rectangles (three fill rectangles, one text rectangle.) Add in the group separators and lines, for maybe an extra two thousand rectangles, and we get the total amount of geometry per frame to be 10 kQuads, or 20 kTris. 20 kTris at 4000 kTris / second would give 200 fps, so the geometry side should work out ok as well.

Then, texture uploads. Suppose that we upload a full set of 2000 128x128x4 thumbnails to the graphics card. The amount of data transferred is 130 megabytes, and (IIRC) the bandwidth is 400-500 MB/s. So we can't plan on uploading the thumbnails on every frame and must have a texture manager. A graphics card is likely to have at least 128 megs of RAM, so if we use 120 megs for the texture cache and upload 10 megs per frame in the worst case, we should be able to sustain 40 fps. It might be worthwhile to crop the small thumbnails on the CPU, which'd make the maximum per-frame transfer be 128x2000x4 bytes, which comes in at 1 MB and sustains >400 fps.

The large thumbnails need to be mipmapped for nice rendering. Doing it with the GL texture mipmap generation extension might be perceptible as it does happen in the drawing thread. Generating the mipmaps at thumbnail load time, keeping them on the CPU, and uploading them manually is likely to cause less drawing latency.

Bringing it all together, a 20 kTri frame of 0.5 Mpx and 1 MB texture uploads would take 5 ms for drawing (1 / min(200 fps, 500 fps)) and 2.5 ms for the texture upload. Giving us a guesstimate worst-case drawing performance of 7.5 ms per frame, and 17.5 ms per frame with the application drawing logic. Which would plonk the framerate to 30 vsynced frames per second. Which I probably can salvage by making the app side faster by 1.5 ms.

So. Yeah. It does sound doable to do the OpenGL side of the drawing fast enough to get 60 fps. Now to see if I can get Tao.OpenGL working. And then theory hits practice for 3d5+3 lightning damage.

Filezoo current status ramble

I had a problem with determining the width of the thumbnail rectangle (it's a mess), but I can just use the thumbnail width for the rectangle instead of the current madness. I think. Maybe.

I'm now using the Gnome thumbnailer's large thumbnails (256x256) as "full size" thumbnails for files that are not jpegs or pngs (which are loaded as pixbufs, rendered onto a Cairo surface and scaled down to 800x800.) It makes the blurry thumbs a little less blurry, which is a little more like it. In a "it stabs your eyes a little bit less"-sense. Cairo's bilinear downscaling makes the 800x800 thumbs look horrible too, a better solution would be to recursively create a mipmap pyramid (scale down to nearest 2^n x 2^n, keep halving until n = 7) and use the nearest larger image to draw the thumbnail. Will cause extra 30% memory use for the large images though.

Large image memory use is.. I'm keeping 20 images cached. Maximum image size is 800x800 = 640000 pixels, 4 bytes per pixel = 2.56 MB. So 51.2 megs for a full large image cache.

Small thumb cache worst case is 260 megs, but it does prune it to 130 megs. Those numbers aren't very happy, I should do thumb cache pruning every N frames and throw all unused thumbs away. So that the thumb cache size approaches the visible set as a function of time. Cheap enough to reload more frequently.

Caching the FSEntries is a bit funny when the kernel pages your cache to the disk. It takes longer to unswap the cache than to recreate it for the visible set.

In the optimal situation I would have no cache and instead rely on the kernel's page cache and the programming language to have the filesystem data accessible fast. Walking a directory tree with 25000 files took 130 ms (in OCaml), so it's not free by any means. Perhaps fast enough to bother trying though? Would simplify my life.

The way how things now work is like this:
  • FSCache is a thread-safe cache that stores filesystem data [and the drawing model in a parallel tree.]
  • FSDraw.PreDraw walks the FSCache and tells it which parts of the drawing model to create. PreDraw also requests loading thumbnails and large thumbnails.
  • FSCache.ThumbnailCache stores normal thumbnails in an LRU cache with max size of 4000 thumbs and starts eliminating old thumbnails when it's larger than 2000 thumbs in size.
  • FSCache.FullSizeThumbnailCache stores the large thumbs and has max size of 20 and eliminates old thumbs when it's larger than 10 in size.
  • FSDraw.Draw walks the FSCache drawing model and draws it out.


PreDraw and thumbnail loaders each use their own threads, Draw is in the GTK event loop thread. The thread data flow is: (PreDraw | ThumbnailCache | FullSizeThumbnailCache | FileSystemWatcher) <-> FSCache -> Draw. The thumbnailer threads and the PreDraw thread fill out the FSCache in parallel. Draw and Click and FindCoverage and other GTK event loop functions read from the FSCache, with a possibly locking read when changing directories (locks if the FSCache doesn't contain the directory already.)

Writes to FSCache lock the whole cache. There is [usually] non-locking read access to the FSCache, and walking the drawing model is lock-free (apart from thumbnails.) The drawing model is always in a drawable state, but the sort and sizes of the files may be inconsistent with the UI setting (they are eventually consistent though.) Thumbnail access uses fine-grained locking: Draw locks the FSEntry with the thumbnail when drawing it, while DestroyThumbnail locks the FSEntry to destroy its thumbnails (drawing a destroyed ImageSurface causes a crash and ImageSurfaces need to be explicitly destroyed, as they don't play nicely with the GC (or I'm doing something wrong.))

The drawing walk takes around 7-10 ms for my home directory, likely more for nasty places like /usr/lib, and the time should be largely independent from activity in the other threads (given infinite hardware resources...) The uncached directory read lock can cause a pause, and should be timed.

Waiting on a thumbnail lock is uncommon on the drawing end, as the amount of code in the lock on the destroy thread is something like two assignments, two Dictionary deletions and two frees. Could time the worst case of 2000 thumbs waiting. Quick napkin math: guess 5000 cycles waiting per destroy, 10 Mcycles for 2000 waits, 20 milliseconds at 2GHz. Which'd give a worst-case scenario of "nasty framerate lapse."

Memory use is bugging me. I think I'll make the thumbnail caches have a "delete old" limit of 0, and see about minimizing the FSCache. While a Nautilus/PCManFM/whatever-like 35 meg memory use may be a distant dream with Mono and the libs eating 20+ megs already, <75 megs should be achievable. Or then I get to rewrite everything in Ada SPARK (yeah, right.)

2008-12-29

Bloodwyne made from elven tears

The thing I dislike most about programming is that it's so difficult to let other people run your programs.

First: library dephell.
Second: they're using a different platform.

How to fix: quit programming, make pictures instead. At least it's a lot harder to make a JPEG that can't be opened on any other machine but yours.

2008-12-28

High-resolution thumbnails


That was pretty simple, if copy-pastey, to do.

Thumb zooming, interaction fixes, user manual


Made thumbs zoom to view width. Next step is to load higher-resolution thumbs when needed. Then make documents appear as directories of pages. And add bookmarks.

Fixed some UI annoyances with throw scrolling (clicking to stop a scroll opened the underlying file, non-throw pan threw anyhow) and wrote a user manual.

Starting work on an OpenGL renderer and text render cache as my framerate bottlenecks (50 ms / frame) are 1) Pango (20 ms) and 2) Cairo (20 ms.) Doing some 2D layout sketches too.

2008-12-26

Filezoo logo sketch


A quick sketch, maybe something like this make a nice logo for Filezoo. Could use GitHub's pages thing for the project website.

Shiny golden GUI growing from roots of steel.

2008-12-24

The old file browser hack


The one on the left is five years old, written in Python, and runs at 60 fps. The one on the right is a month and a half old, written in C#, and runs at 15 fps. Looking at that screenshot makes me think that i've wasted five years doing everything else but what I should've been doing. Which is make the Python hack into a production-quality app.

Day 40-41: more tests, return of the dark theme


Split prelude.ml into small bits. Took hours and yielded very little in the way of benefits. Everything's still interlocked to all hell and back, except that now there's more code duplication and it's harder to load. I think I'll port the new things (uniform collection interfaces) from the split into the single file version and get rid of the split version.

Wrote tests for Filezoo Helpers utility class yesterday. Uncovered a bunch of bugs, as expected. Could move to something fancy for handling filesystem ops, which'd net me smb:// and sftp:// and whatnot. The problem is that Gnome.VFS is deprecated, GVFS doesn't work and KDE is doing their "we're rewriting the world from scratch!!"-madness. So I don't have any good options.

While the white theme was nice, my FVWM desktop is gray20. And I can't use a gray20 background for GTK as their widget themes aren't built for that. Not to mention that I can't trust GTK's default widget style colors, they're always white for bg and black for fg. So screw respecting the GTK theme, I'll just hardcode the colors.

Compiz is nice and fast and mplayer works right with it, but you can only use metacity with it, which means that it's useless to me. Options? I want drop shadows that don't break mplayer (as xcompmgr does.)

Also, played with my ages-old (circa 2003) OpenGL filesystem browser a bit and it really underlined the slowness of the current Cairo+Pango-renderer. 10-20 FPS feels okay when your comparison is other slow desktop apps, but compared to an app that runs at 60 FPS, the difference is painful.

Less bitching, more coding. Features I'd like to implement over the next weeks:
  • scale thumbnails to screen width when zoomed close
  • read (text/pdf/ps) documents inline
  • UI optimization tools
    • UI profiling ("it took you n seconds between zooms", "it took you m seconds to change directory")
    • A UI benchmarking game ("find [some file definition], select it", "select [some pattern] files", etc.)
  • browsing SFTP
  • port the renderer over to OpenGL
  • test the app on a netbook
  • make the panel work as a Gnome panel applet
  • use a config file for the menus, icons and colors
  • make it prettier


Merry Christmas!

2008-12-21

Day 39, part 2: two tests, two fixed methods, video

Started writing tests for Helpers. Wrote a test for Touch and another for MkdirP and then had to go and fix them. And the problems spring from path escaping failing to cope with whitespace correctly. Now it escapes special characters, while it should single-quote the path and escape single-quotes. Which is all very "execve, do you speak it?", the string mudballing probably coming from Windows being retarded with its commandlines. Or that's what I heard on the Internet.

Have a small video of animated scroll wheel zoom.

Day 39: white theme, drawing ramble


Doing a readable white-bg version of the theme, played around a bit with rgba visuals to make the window transparent (simple enough but makes for a bit unreadable file view. A shader blur would help but eh, how do I do that?) Also made it draw a good deal less (previously the "do I need to draw this tiny thing"-check was doing using quarter sample checking (~16x AA), now doing half sample checking (~4x AA), effectively halving the maximum amount of rectangles drawn. It's still noisy and moire-y and I'd have to write some simple test cases to find the right offset, if there are any. Or do some manual overdraw to blur them together, which still wouldn't help popping flicker. Maybe do the size vis check in a deterministic fashion, independent of pan, and have it only add visible things when zooming in. Yeah.)

Tests. Right. I'll get right on them. Starting with everything tagged DESTRUCTIVE. One test here, another there, small steps~

Taking tomorrow off to work on some neglected projects (gitbug needs less conflicty bug ids, prelude.ml should be whacked with a sanity mallet and split into several independent files.) And maybe write some pretty OpenGL hack?

2008-12-20

Day 38, part 3: NUnit test tests, some small group title tweaks


Such great tests, yes?

Tweaked the group titles, now they fade in and out in a smooth fashion. The date titles use "4.2008"-format (should probably be logarithmic scale but eh. Year last to right-align the most significant info), size titles are "100 kB+" instead of "100,0 kB" and the type titles show upper-case first letter for files without suffixes, so it doesn't screw up with directory navigation as much.

Tomorrow: real tests, refactor actions to a separate class (filezoo.cs (52k), helpers.cs (30k), fscache.cs (24k), fsdraw.cs (26k) and contextmenu.cs (15k) are growing like tumors, need to do some splitting to keep them manageable.)

Filezoo, day 38, part 2: group titles


First implementation on sort group titles, draws little group title to the right side of the screen. It doesn't have scaling or hiding by group size, so there's lots of overlap. And I don't know if I'm going to put the titles visible in subdirs as well. It'd be nice but how to make it look nice and work nice and... oh, now I know [see below.]

Results for the navigation benchmark (homedir to /usr/lib/OpenGL/ghc): can now achieve sub-7 second times consistently with the scroll wheel zoom, and click navigation times are in sub-6 second range. Would be nice to have some sort of UI benchmarking suite that gives me random navigation tasks and toggles features [and optimizes the UI to my physical ability and searches for an optimal UI and does my homework.]

Edit: new and improved group titles.

2008-12-19

Filezoo, day 38, part 1: bug tracker, plan for using NUnit and monocov


Used gitbug to set up a simple in-repo bug tracker. Gitbug isn't perfect for managing larger projects (id conflicts are possible as there's no global clock.) But as Filezoo has only one developer, it is perfect.

Added one bug that says "set up NUnit and write some tests for things you don't know whether they work right or not" and an another with "write a user manual and eliminate features that must be in the manual, then throw away the manual."

Going to do the first one today (the setting up NUnit -part.) Wish me luck!

Navigation timings with smooth zoom

Oh, right, guess I should start by telling you that Filezoo has now smooth zooming with the scroll wheel as well.

Anyhow, timed navigation from /home/kig to /usr/lib/OpenGL-2.2.1.1/ghc-6.8.2 with zoom and the go-where-I-want clicking.

Zoom times were usually in the 13-15 second range, as the probability for error was high with them. The best zoom times were in the 9-10 second bracket, I think I got a couple times under 9 seconds as well.

Clicky times were about 10-13 seconds for the first tries, then improved to around 7 seconds, with best in around 6 seconds. The clicking is less error-prone and has a smarter zoom.

As noted in the previous post, the time for typing the path with tab-completion is around five seconds. A realistic minimum time for the mouse navigation might be around 4 seconds, as that's the time it takes to do "move the mouse and click" six times.

Filezoo plans hodgepodge

Okay, so I have this quite nifty, if unpolished memory hog of a file manager. Where to go from here? What are my goals?

The goals I have for Filezoo are: 1) taking care of casual file management and 2) looking good.

Putting the second part aside, since it's just a matter of putting in months of back-breaking, err, finger-callousing graphics work and design innovation, let's focus on casual file management.

Casual file management

Casual file management is about having an always-on non-intrusive file manager at your beck and call in a place where you can summon it from with the flick of a wrist.

How I've been doing casual file management is roughly:
  1. Hit F1 to bring up a terminal window
  2. Move mouse cursor to the window
  3. cd whateverdir
  4. lr (ls -rt) or ll (ls -l)
  5. Shift-PgUp/PgDn to find whatever it is that I'm interested in
  6. Type command and tab-complete
  7. Hit enter
  8. Ctrl-D the terminal

The problem with using the shell for casual file management is that the shell does a pretty dreadful job at visualizing the filesystem and it's difficult to select files while reading ls output. The shell doesn't really do file system monitoring either. And it isn't pretty.

The shell does some things very well though. For one, it has a complete language and set of tools for operating on lists of files. Using the shell doesn't require moving your hands off the keyboard either[1]. And it has command history and tab-completion and loops and variables. Maybe it would be a good idea to have an embedded terminal like Dolphin et al., except put command output in a some sort of non-intrusive log window.

Random UI plans

Drag'n'drop and copy-paste both work by sending a source to a target with some action. possible_actions : source -> target -> (source -> target) list. When doing common interactions, pick the first action without asking and have the first action be the same regardless of target type. Dragging the selection to a directory would move it there, for instance. Use possible_actions to build the context menus.

Build a consistent vocabulary for doing things. Prune the menus of things that are better accomplished with drag'n'drop / copy-paste (you still have the shell for the non-dnd/copypaste-ways.)

Go hunting. Use different file managers and find out their good things and what makes them tick, then create a synthesis of the best features. Old programs still in active use might be a good source: you have to be doing something right to survive the vagaries of time. Perhaps clones of old programs would be an even better source, less prone to "this is what I've always used and always will, doggonit."

Profile the UI[2][3] and build the rest of the program to maximize UI effectiveness (including error recovery times.) Find the top-level tasks, decompose them into primitive operations and rebuild them.

Conclusion

The first month was about staking out a vision. The second month will be about achieving feature parity and a consistent user interface. The third month will be about optimizing the user interface with the power of Science into the fastest thing there ever was. And the fourth month will be pure art. Right. I give this plan a 7% chance of survival.

[1] In terms of navigation speed to known places, touch-typing and point'n'click are roughly equal, so the winner is the one that requires fewer context switches. If you have the mouse in your hand, point'n'click. If you're on the keyboard, type.

[2] Some empiric results (using the ghetto method of time read, do stuff, hit enter, deduct one mouse-kb-context switch time):
  • Moving hand from kb to mouse and back: 1.5 s
  • Hitting Ctrl-AnyKey: 1.5 s
  • Hitting a F-key: 1 s
  • Flicking mouse to screen corner: 0.5 s
  • Typing code/repos/filezoo: 3 s
  • Clicking code->repos->filezoo: 3 s
  • Typing /usr/lib/OpenGL-2.2.1.1/ghc-6.8.2/ with tab-complete: 4.4 s
  • Clicking to /usr/lib/OpenGL-2.2.1.1/ghc-6.8.2/ from home dir: Filezoo: 7 s, Konqueror: 11-14 s, Nautilus: 11 s, PCManFM: 10 s, Xfe: 9 s, Dolphin: 10 s. The difference in times comes from the the time it takes to search for OpenGL-2.2.1.1 in /usr/lib and the time it takes to open /usr/lib. A more specific UI profiler would help.
  • Selecting downloads/Doug Engelbart 1968 demo.avi: Filezoo: 4.2 s, Nautilus: 3.8 s, downloads/TL58.mp4: Filezoo: 4.3 s, Nautilus: 4.4 s.


[3] The CogTool project - tools for cognitive performance modeling for interactive devices

Filezoo day 35-37: better context menus, Fitts's law in breadcrumb, wacky single-button navigation

Lots of fancy stuff recently. Moved to using Gnome's thumbnail factory for the thumbs. Which means video thumbs and font thumbs, etc.

Also went over the context menus and removed some superfluous entries ("Copy to..."), rearranged menu items (e.g. moved "Move to trash" away from the bottom-most element, replaced the bottom item with a non-destructive one to decrease the impact of accidental menu invocations), added separators and type-specific handlers for selections, and image rotation commands.

The context menu is still not quite done but it's getting pretty useful. It doesn't have submenus (apart from the under-mouse-submenu when there's a selection), which is good. Submenus are finicky to use and their contents invisible. And the size constraint imposed by the lack of submenus acts as selection pressure towards a better set of menu items. I'd like a pie menu. A common desktop-wide pie menu that works the same in all apps.

The breadcrumb bar at the top of the window now has its active area spanning all the way to the topmost pixel of the window (top of screen in panel mode.) Now you can throw your mouse up and click and ooh, success!

Also made a smoothly zooming and panning middle-button drag navigation thing. It makes the app nicer to use with a Wacom tablet. It's very finicky though, which I need to fix.

Also added inertia scrolling, a.k.a. throw-panning. What is the name of that thing? It's nifty though. And works with zoom. And works great with the new seamless zoom and pan. Want to go to the next dir? Throw the screen upwards and it pans there. Previous dir? Throw downwards. Click or hit a key to stop.

I'm writing a plan for month two but it's gotten stuck in some limbo of literary self-crit.

2008-12-15

One month recap in images

Week one

Week two

Week three

Week four

Week five

Filezoo day 34, part 2: keyboard shortcuts, dragging files


Did I show this mockup already? If not, great!

Anyhow. Got ctrl-x, ctrl-c, and ctrl-v going (though in a very manual fashion, I guess the right way to do it would be AccelGroups?) and added some other keyboard shortcuts (delete to trash, backspace to parent dir, home to home dir.) Made the mouse cursor change depending on what modifiers you have down. And got dragging working. And it was a pain. So I'm going to share the pain in the form of a couple dozen lines of code.

In button press handler:

if (e.Button == 1) {
int w, h;
e.Window.GetSize (out w, out h);
DragSourceEntry = FindHit((uint)w, (uint)h, e.X, e.Y, 8).Target;
DragSourcePath = DragSourceEntry.FullName;
}

In button release handler:

if (e.Button == 1) {
if (!dragInProgress) {
DragSourceEntry = null;
DragSourcePath = null;
}
dragInProgress = false;
}
dragging = false;
panning = panning && dragInProgress;

In motion handler:

bool left = (e.State & Gdk.ModifierType.Button1Mask) == Gdk.ModifierType.Button1Mask;
bool middle = (e.State & Gdk.ModifierType.Button2Mask) == Gdk.ModifierType.Button2Mask;

if (middle) {
dragging = true;
panning = true;
}
if (left) {
dragging = dragging || ((Math.Abs(dragX - dragStartX) + Math.Abs(dragY - dragStartY)) > 4);
panning = panning || (dragStartX > 128+FilesMarginLeft);
if (!dragInProgress && !panning && dragging) {
Gdk.DragAction action = Gdk.DragAction.Move;
if ((e.State & Gdk.ModifierType.ControlMask) == Gdk.ModifierType.ControlMask)
action = Gdk.DragAction.Copy;
if ((e.State & Gdk.ModifierType.ShiftMask) == Gdk.ModifierType.ShiftMask)
action = Gdk.DragAction.Copy;
if ((e.State & Gdk.ModifierType.Mod1Mask) == Gdk.ModifierType.Mod1Mask)
action = Gdk.DragAction.Ask;
dragInProgress = true;

Drag.Begin (this, new TargetList(targets), action, 1, e);

SetCursor (e.State);
}
}

And the drag event handlers:

DragDataGet += delegate (object o, DragDataGetArgs args) {
string items = "file://" + DragSourcePath;
if (Selection.Count > 0 && Selection.ContainsKey(DragSourcePath))
items = GetSelectionData ();
args.SelectionData.Set(args.SelectionData.Target, 8, System.Text.Encoding.UTF8.GetBytes(items));
args.SelectionData.Text = items;
};

DragEnd += delegate {
GetSelectionData ();
dragInProgress = false;
DragSourceEntry = null;
DragSourcePath = null;
};

Such fun, yes? Also, read this.

I still don't know how to change the action in response to modifier key state changes (try dragging something in Konqueror and pressing and releasing ctrl/shift/alt.)

Tomorrow, I think it's time for a recap. Everyone just loves recap episodes, don't they? Not to mention that this blog is like the worst sitcom ever made.

2008-12-14

Oh. List has functional iterators.

I've been doing things like:

List<int> l = new List<int>() {1,2,3,4,5,6,7,8,9};
List<int> m = new List<int>();
foreach (int i in l) if (i >= 5) m.Add(i);
foreach (int i in m) Console.WriteLine(i);

When I could've done:

var l = new List<int>() {1,2,3,4,5,6,7,8,9};
l.FindAll(x => x >= 5).ForEach(Console.WriteLine);

Why didn't I figure this out a month ago.

Filezoo day 34, part 1: extended context menu, cut-copy-paste


Got cut-copy-paste business logic done, still missing the keyboard shortcuts though. The context menu is getting huge!

Patching up yesterday's missed todo items today, next up are clipboard keyboard shortcuts and dragging items.

The clipboard stuff was a bit of a pain, so here's a reference. I don't know if I'm doing it in The Right Way, but it does work. Can paste files to/from Konqueror at least.

void BuildCopyPasteMenu (Menu menu, ClickHit c)
{
string targetPath = c.Target.FullName;
string targetDir = c.Target.IsDirectory ? c.Target.FullName : Helpers.Dirname(c.Target.FullName);

Separator (menu);
string items = "file://" + c.Target.FullName;
if (App.Selection.Count > 0) {
items = App.GetSelectionData ();
}

AddItem(menu, "Cut", delegate {
SetClipBoard(items);
cut = true;
});

AddItem(menu, "Copy", delegate {
SetClipBoard(items);
cut = false;
});

AddItem(menu, "Paste to "+Helpers.Basename(targetDir)+"/", delegate {
bool handled = false;
clipboard.RequestContents(Gdk.Atom.Intern("text/uri-list", true), delegate(Clipboard cb, SelectionData data) {
if (data.Length > -1) {
handled = true;
App.HandleSelectionData(data, cut ? Gdk.DragAction.Move : Gdk.DragAction.Copy, targetPath);
cut = false;
}
});
clipboard.RequestContents(Gdk.Atom.Intern("application/x-color", true), delegate(Clipboard cb, SelectionData data) {
if (data.Length > -1 && !handled) {
handled = true;
App.HandleSelectionData(data, cut ? Gdk.DragAction.Move : Gdk.DragAction.Copy, targetPath);
}
});
clipboard.RequestContents(Gdk.Atom.Intern("text/plain", true), delegate(Clipboard cb, SelectionData data) {
if (data.Length > -1 && !handled) {
handled = true;
App.HandleSelectionData(data, cut ? Gdk.DragAction.Move : Gdk.DragAction.Copy, targetPath);
}
});
});
Separator (menu);
}

void SetClipBoard (string items)
{
clipboard.SetWithData(targets,
delegate (Clipboard cb, SelectionData data, uint info) {
data.Set(data.Target, 8, System.Text.Encoding.UTF8.GetBytes(items));
data.Text = items;
},
delegate (Clipboard cb) {
cut = false;
}
);
}

Filezoo day 33, part 2: more plugged leaks, selection and selection context menu


Plugged the ImageSurface leak (if it was such) and made do with less caching. Now the memory usage stays below 100 megs with the normal measurers and rises to 200+ megs with big recursive traversals. What worries me is that A) 200+ megs is still a huge fucking lot of memory for a glorified ls, even nautilus uses just 45 megs, and, B) the memory use doesn't fluctuate, it just rises.

And that plugfest blew a hole in my start-of-day plans. As you can see in the screenshot above, I got the selections working (ctrl-select, shift-select, alt-click to deselect) and made a little preliminary context menu.

Drag and drop dragging, the logic from the app pov should be easy, I could even draw a pixbuf of the selection to show when dragging. Cut-copy-paste, mmm, I'll try and reuse the dragging handlers. What remains is finding out how to use the clipboard.

Sleep now~

2008-12-13

Filezoo day 33, part 1: memleak fixes


Had a few memleaks in the drawing loop. Pango.CairoHelper.CreateLayout leaks, especially when coupled with Pango.CairoHelper.ShowLayout. So now I'm using a lot of static variables in the text drawing path, which means that it's not thread-safe at all and I'm going to get bitten by it at some point. Creating gradients outside a using-block leaks as well. But now those are fixed and I can draw thousands of frames without the memory usage moving up from 40 megs.

I don't know if my thumbnail ImageSurfaces are space leaks. I do Destroy them. And yet I did dispose the Pango.Layouts as well. I couldn't push memory usage beyond 500 megs though, even with 4000+ thumbnails loaded (but eh, that's not a very rigorous test.) I think they do leak, since once I get mem use up to 300 megs with the thumbs, it's not coming back down.

The thumbnails in the image above take ridiculously long to load too. There are only 3300 drawn. Loading 3300 128x128 thumbnails should take... 0.75 ms per thumb, 2.5 seconds. Oh wait, those would be JPEG thumbnails. PNG thumbs, forever, yeah. I have just the thing for this thumbnail loading and drawing stuff though.

I also added some cache pruning. Now traversal cache is cleared when switching measurers. Which is a hack to enable manual pumping for correct information. And it shouldn't really be there in a perfect world.

In other news, now there's a bit dubious feature when drag-dropping text onto dirs and files. When you drop text onto a dir, it asks whether you want to create a new file. If you drop text on a file, you can either create a new file in the dir, replace the file contents with the text, or append the text to the file. Like mouse-driven xsel -o > file and xsel -o >> file.

Plan for the rest of the day:
  • Cut-Copy-Paste handling
  • Selecting files
  • Dragging files and selections

Filezoo day 32: drag and drop handling

Added a handler for dealing with data dropped onto the Filezoo window. It copies or moves the source uri list (Konqueror, Nautilus, Firefox and others pass the DnD'd files as \r\n-separated strings of URIs) over to the drag target directory using Gnome-VFS. So now I can drag an image from Firefox onto a Filezoo directory and it'll be copied there.

The implementation was a wee bit challenging, since there was very little documentation for doing DragDrop event handling and perhaps even less for the Gnome.Vfs namespace. Here's what I used:

Handling data dropped on the widget, this is from the widget constructor:

TargetEntry[] target_table = new TargetEntry [] {
new TargetEntry ("text/uri-list", 0, 0),
new TargetEntry ("text/plain", 0, 1),
new TargetEntry ("STRING", 0, 1)
};

DragDataReceived += delegate (object sender, DragDataReceivedArgs e) {
string type = e.SelectionData.Type.Name;
Console.WriteLine(type);
Gdk.DragAction action = e.Context.SuggestedAction;
if (type == "text/uri-list" || (type == "text/plain" && IsURI(e.SelectionData.Text))) {
string data = new System.Text.ASCIIEncoding().GetString(e.SelectionData.Data);
string[] uris = data.Split(new char[] {'\r','\n','\0'},
StringSplitOptions.RemoveEmptyEntries);
string msg = action.ToString() + " " + String.join(", ", uris);
} else {
Console.WriteLine("Got {0}", e.SelectionData.Text);
}
);

Gtk.Drag.DestSet (this, DestDefaults.All, target_table,
Gdk.DragAction.Move
| Gdk.DragAction.Copy
| Gdk.DragAction.Ask
);

Using Gnome-VFS XferUriList to do a copy or move:

public static void XferURIs
(Gnome.Vfs.Uri[] sources, Gnome.Vfs.Uri[] targets, bool removeSources)
{
XferURIs (sources, targets, removeSources, ConsoleXferProgressCallback);
}

public static void XferURIs
(Gnome.Vfs.Uri[] sources, Gnome.Vfs.Uri[] targets, bool removeSources,
Gnome.Vfs.XferProgressCallback callback)
{
Gnome.Vfs.Vfs.Initialize ();
Gnome.Vfs.XferOptions mode = Gnome.Vfs.XferOptions.Recursive;
if (removeSources) mode = mode | Gnome.Vfs.XferOptions.Removesource;
Gnome.Vfs.Xfer.XferUriList (
sources, targets, mode,
Gnome.Vfs.XferErrorMode.Query,
Gnome.Vfs.XferOverwriteMode.Replace,
callback
);
}

public static int ConsoleXferProgressCallback (Gnome.Vfs.XferProgressInfo info)
{
switch (info.Status) {
case Gnome.Vfs.XferProgressStatus.Vfserror:
Console.WriteLine("{0}: {1} in {2} -> {3}",
info.Status, info.VfsStatus, info.SourceName, info.TargetName);
return (int)Gnome.Vfs.XferErrorAction.Abort;
case Gnome.Vfs.XferProgressStatus.Overwrite:
Console.WriteLine("{0}: {1} in {2} -> {3}",
info.Status, info.VfsStatus, info.SourceName, info.TargetName);
return (int)Gnome.Vfs.XferOverwriteAction.Abort;
default:
Console.WriteLine("{0} / {1} {2} -> {3}",
info.BytesCopied, info.BytesTotal, info.SourceName, info.TargetName);
return 1;
}
}

(Now I need a pimpin' progress indicator... maybe an arc to the dir with "incoming $STUFF -." or something with ambient notifications. Maybe just pop up a plain jane Gtk progress dialog :P)

Next features on this path would be dragging things around, navigating while dragging (think Finder's space-to-descend on steroids), and handling cut-copy-paste.

2008-12-12

Simple clock in F# and Cairo


Ported the C# Simpleclock (see previous writeup) over to F# to get to grips with the language and the tools. The resulting Simpleclock.fs is around 10 lines shorter than the C# version and includes a currying-style wrapping over most of basic Cairo.

I like how .NET method arguments are tuples in F#. Having to add type annotations for object parameters is less nifty.

let curry5 f x y z u v = f (x,y,z,u,v)

let arc (cr:Cairo.Context) = curry5 cr.Arc

I don't much appreciate how fscp10.exe takes 5 seconds to start up either.

F# is seriously cool though. I should try porting some parts of Filezoo over and link them in.

2008-12-11

The reason i dig functional languages over C#/Java

Consider this C# snippet to stroke a polygon:

public static void StrokePolygon (Context cr, List<PointD> points)
{
DrawPolygon (cr, points);
cr.Stroke ();
}

public static void DrawPolygon (Context cr, List<PointD> points)
{
cr.NewPath ();
bool first = true;
foreach (PointD p in points) {
if (first) {
first = false;
cr.MoveTo (p.X, p.Y);
} else {
cr.LineTo (p.X, p.Y);
}
}
}

And its Haskell equivalent:

strokePolygon = strokeWith drawPolygon
drawPolygon p = do { newPath; drawSubPolygon p }
drawSubPolygon (x:xs) = do { moveToP x; addToPolygon xs }
addToPolygon = mapM_ lineToP
strokeWith = doWith stroke
doWith g f x = do { f x; g }
moveToP = uncurry moveTo
lineToP = uncurry lineTo

The C# version is 379 characters long. The Haskell version is 283 characters long. The C# version defines two reusable functions. The Haskell version defines eight.

In the same amount of characters it takes to write a single function in C#, you can (and usually will) write a whole library in Haskell.

To really make the comparison, consider adding functions to draw filled polygons and filled and stroked circles.

public static void FillPolygon (Context cr, List<PointD> points)
{
DrawPolygon (cr, points);
cr.Fill ();
}
public static void FillCircle (Context cr, PointD c, double r)
{
DrawCircle (c, r);
cr.Fill ();
}
public static void StrokeCircle (Context cr, PointD c, double r)
{
DrawCircle (c, r);
cr.Stroke ();
}
public static void DrawCircle (Context cr, PointD c, double r)
{
cr.NewPath ();
cr.Arc (c.X, c.Y, r, 0, Math.PI*2);
}

And the Haskell version:

fillPolygon = fillWith drawPolygon
fillCircle c = fillWith (drawCircle c)
strokeCircle c = strokeWith (drawCircle c)
drawCircle c r = do { newPath; addCircle c r }
addCircle (x,y) r = arc x y r 0 (pi*2)
fillWith = doWith fill

444 characters in C#, 226 in Haskell. Six functions in Haskell, four in C#. The thing holding back C#'s character count is the heavy function signature boilerplate. In each of the four C# functions above, the function signature has more characters than the function body.

Filezoo day ...I don't know, let's say 31.

Filezoo. A few things.

Had this random hang at application startup. I don't know what was causing it, but disabling the AOT compilation pass made it disappear. So there.

Config now has hooks for using the Gtk theme colors for the application background et al. But I don't have a way of drawing pretty directories for light backgrounds so I disabled it. And black on white directory labels really make URW Gothic L cry.

Moved the panel controls to a whole new widget. Added the widget to the stand-alone windows. Hurrah for DWIM bar!

Some UTF-8 filenames can crash the renderer. I don't know wtf is up with that. On reaching a certain size, the font just completely blows up and the render target _vanishes_.

I didn't really have any particular goal for this day, maybe it's time to start doing the one-month roundup and really start focusing on the UI side of things for the next month.

On missing features. There's no scrollbar, there's no way to zoom to readable size from the keyboard, there's no way to control view settings from the DWIM bar. Directory view settings aren't remembered. The zoom out navigation is jumpy and you can't pan between dirs that are siblings to the current dir. It would help if text files were rendered inside their boxes. Having some sort of extended preview / reading functionality would be cool, it's often handiest to do read-in-place and edit-in-place instead of "pop up an application that hopefully opens your file and wait for ten seconds while it loads." Eaglemode does it the right way, yes.

Making a nicer theme system might be useful. And configuration and menu scripting. Saving config to GConf and durr. It's kind of hard to find the willpower to work on that stuff.

The core rewrite. Hmm. I would like it to be done. And I really wouldn't like to do it.

Something I think I will do (totally lying here) is port the renderer over to OpenGL. And bring back the lens flare. A shining 100FPS or bust!

2008-12-10

Drawing trees with Haskell and Cairo


I'd like to draw a tree, but how? Let's start by writing a simple draw loop:

module Main where
import Graphics.Rendering.Cairo
import Canvas

main = canvas draw 600 600

draw w h t = do
color white
rectangle 0 0 w h
fill
color black
drawTree w h t

Then, what is a tree? A tree is sort of a recursive forking function. A scanl towards the sun. Every year its branches grow in thickness, possibly forking.

Let's define a simple branch as a function of age and angle. A branch of age 0 has no forks. A branch of age N has 2 sub-branches of age N-1.

branch 0 angle = [map (rotateP angle) [(0,0), (0, -1)]]
branch n angle =
this ++ subBranches
where
this = branch 0 angle
[[_,(x,y)]] = this
subBranches = map (map (translateP x y)) (left ++ right)
left = branch (n-1) (angle-pi/8)
right = branch (n-1) (angle+pi/8)

To draw the branches, we need to write the drawTree procedure. Here's one that draws a tree of age 7 and rotates it in the middle of the screen:

drawTree w h t = do
translate (w/2) (h/2)
rotate t
mapM_ strokeLine tree
where tree = map (map (uscaleP 25)) $ branch 7 0

You can see the result on the right. Not the prettiest tree in the land. Let's make the branches get thicker with age.

To draw lines of different thickness, we need to add the thickness information to the line data structure. Previously it was a list of (x,y)-tuples, with width it becomes a (lineWidth, (x,y) list)-tuple. A couple combinators will help here:

strokeWidthLine = tupleDo lineWidth strokeLine
mapWidthLine f = fupleR (map f)

fupleR f (a,b) = (a, f b)

Then rewrite branch and drawTree to use width-carrying lines:

drawTree w h t = do
translate (w/2) h
mapM_ strokeWidthLine tree
where tree = map (mapWidthLine (uscaleP 25)) $ branch 8 0

branch 0 angle = []
branch n angle =
(thickness, points) : subBranches
where
points = map (rotateP angle) [(0,0), (0, -1)]
thickness = n
[_,(x,y)] = points
subBranches = map (mapWidthLine (translateP x y)) (left ++ right)
left = branch (n-1) (angle-pi/8)
right = branch (n-1) (angle+pi/8)


Now the tree grows from the bottom of the screen and looks a bit more aesthetically pleasing. Next we could make the branches rotate and grow with an upwards bias. Compute distance from up-vector and scale points and the angle accordingly. Something like this:

da = angularDistance 0 angle
scale = 3 * ((1-(abs da / pi)) ** 2)
points = map (rotateP (angle + da/3) . uscaleP scale) [(0,0), (0, -1)]

And then, hmm, random angles for the branches? That needs a bit of extra work. The random number generator is in the IO monad, whereas draw is in the Render monad, and branch is a pure function. So, extend main to get a pure list of random Doubles, then pass that to draw, which passes it to drawTree and branch.

main = do
gen <- getStdGen
let ns = randoms gen :: [Double]

canvas (draw ns) 600 600

draw ns w h t = do
color white
rectangle 0 0 w h
fill
color black
drawTree ns w h t

drawTree ns w h t = do
translate (w/2) (h+5)
mapM_ strokeWidthLine tree
where tree = map (mapWidthLine (uscaleP 25)) $ branch ns 8 (pi/2*sin t)

And make branch do something with it:

branch _ 0 _ = []
branch (r1:r2:rs) n angle =
[...snip...]
left = branch (takeOdd rs) (n-1) (angle - r1*pi/4)
right = branch (takeEven rs) (n-1) (angle + r2*pi/4)

takeOdd [] = []
takeOdd [x] = []
takeOdd (_:x:xs) = x : (takeOdd xs)

takeEven [] = []
takeEven [x] = [x]
takeEven (x:_:xs) = x : (takeEven xs)

The result of all this tomfoolery is a tree that looks a bit more natural than the geometric trees above.

The trees at the top of this post use random numbers for scaling the branches as well, so they're even more noisy.

Here's the source code: tree.hs and canvas.hs.
Compile by doing ghc --make tree.hs canvas.hs -o tree

Filezoo, day 30-ish: painted myself in a corner

Ok, committed a few bugfixes to the traversal code, and now the size totals work pretty nicely, though likely allocate way too much. A bigger problem is that the current FSCache design is suffering from a couple decisions made at last rewrite time. In particular, the drawing model uses the FSCache directly, which means that it's race-condition-prone. If a filesystem change happens during drawing a frame, what gets drawn beyond that point is anyone's guess. It'll be fixed by the next frame though.

The New And Improved Design Suffering From Version 2 Syndrome I have is to split the FSCache into three caches instead of the current two: Entry, Thumbnail -> Entry, StatInfo, Thumbnail. The idea being that Entry would be a very light-weight thing used by the recursive traversal, and StatInfo contains all the stat struct information. StatInfo cache and Thumbnail cache would have LRU expiration to limit memory use. The Entry cache wouldn't really need expiration, as it's not going to be much larger than 20 megs (but it is a possible memory leak...)

Then the drawing model builder wouldn't use the FSCache directly, but build a temporary drawing tree that's always ready to draw. Currently you can get drawing flicker on filesystem changes and traversal changes because the changes set ReadyToDraw = false for that part of the tree, and the model builder needs to revisit and redo the layout to make it drawable again.

Building a drawing tree separate from the FSCache would allow propagating the FSCache changes to the drawing model in a controlled manner, so that the tree is always in a drawable state and there's no flicker and you can't have race conditions involving filesystem changes and the drawing thread.

Why the new design suffers from V2S is that it requires a well-nigh complete rewrite of the cache behaviour and the draw model building behaviour. Yesterday I wrote a few hundred lines of code in an effort to do the split, but as I didn't do it by small compilable changes, it very quickly veered into the "I'm never going to get this to work"-state of lost confidence. Lesson learned once again.

So. Stashed the changes in a branch, forked a new branch from master and backported some of the small changes back. I also fixed some buggy behaviour, now filesystem changes aren't as likely to hose the drawn state and UTF-8 filenames are handled correctly by traversal (yay for new System.Text.UTF8Encoding().GetString(byte[]).) I think I'll spend the rest of the day doing something with a high return on investment, a.k.a. UI work.

Then piecemeal porting of FSCache over to a cleaner system? Don't break the build!

2008-12-08

Distorted hex


Perspective projection gone horribly wrong. The gist of it is

cylinderProjection r (x, y) =
(mx * 200/z, my * 200/z)
where mx = r * sin (x/r)
my = y
z = 300 + r + r * cos (x/r)

Here the source: distorted_hex.hs

Blog Archive