issie icon indicating copy to clipboard operation
issie copied to clipboard

Better SVG creation

Open tomcl opened this issue 10 months ago • 5 comments

This is motivated by #442

Currently, whenever SVG radix or zoom is changed, waveform SVGs are recreated. We recreate (typically) 1000 cycles of the SVG even if we are only looking at 2 cycles.

The proposal is to recreate a custom segment of SVG waveform with a width that exactly fits the viewer. This will typically be only 20 cycles and at most around 300 cycles - however in that case there will be no text printed on waveforms which will consist of two polylines each only.

Advantages:

  • Fixes #442
  • Faster, more responsive, waveform generation
  • Numeric values can be printed, when there is room, in the correct places - at either side of a waveform change or at the edge of the viewer. this will make the waveforms more readable.

Implementation

  1. Waveform generation happens in generateWaveform. There is some code above this that calculates when waveforms need to be regenerated - this may need changing as well (to regenerate more often) but that will be quite easy to fix.
  2. Inside generateWaveform there are 3 match cases according to waveform width: binary, <= 32 bits, > 32 bits. The logic is different for binary (waveform shows value) and other (waveform shows when value changes - value is written on waveform as text).
  3. In all cases waveforms are generated first by extracting a transitions array transitions from the simulator, then by generating polylines (for waveform) and if required text labels from this.
  4. the transitions array is generated by calculateBinaryTransitionsUnInt32 or calculateNonBinaryTransitions. Currently this is always 1000 cycles long. These functions must be changed to calculate only the clock cycles currently in the viewer: ws.ShownCycles, ws.StartCycle
  5. The functions that add text: displayUnint32Onwave, displayBigIntOnwave should be changed:
    • to display only 0, 1 (in middle) , or 2 (at either end) copies of number according to width of gap rather than displaying many repetitions.
    • To use a newly written fast function upperBoundOfNumericTextWidth (t: Text) (r: radix) (txt: string). This function is curried and can be written so that all the necessary calls to textMetrics are done just once when it is called with a constant first parameter.

tomcl avatar Mar 30 '24 15:03 tomcl

Context for SVG creation:

  • Maybe we will want to display in-integral-clock-cycle SVGs when scrolling to have better UI. In that case we will still (I think) have a snap-to-integral thing at the end of a scrollbar drag.
  • We do not know how fast creation will be. it is plausible to think it will *always be fast - but only if we replace oscillatory bits of SVG with too many points to see with grey polygons. this would make waveforms look better as well.
  • SVG text - this needs to be written in the correct place - which depends on what you can see on screen. Best would be as 5 above.
    • See #449 - I tried implementing some of this, it was not much faster.

tomcl avatar Apr 13 '24 10:04 tomcl

Long Term Plan (for ideal display).

  • Create shorter SVGs
  • Optimise text placement and grey polygon placement on SVG
  • Create scroll bar - use an svg polygon to slide. 1. work out where to put it - add a dummy svg there 2. work out how to display a scrollbar as an svg. 3. work out how to capture clicks and mouse moves on the svg (see draw block, also see context menus which capture right-clicks on any issue html component. 4. Work out how should scrollbar work when scrolling much longer distances than can be seen. 5. Make it work 7. Check worst case timing - of a problem instrument this. 8. Implement "fluid" scroll.

tomcl avatar Apr 13 '24 10:04 tomcl

Stage 1: Shorter SVG

Summary

  • Shorter SVG generation was achieved by truncating the transitions array, and shifting displayed numbers.
  • Solution's performance was analysed, see results below.
  • Ability to generate full SVG was left in.

Performance Analysis

The function being timed was the makeWaveformsWithTimeOut function. A quick experiment was done run on 107 waves with 38 binary and 69 short non-binary waves. Waveform Simulator was set to generate 8 cycles from the start (0th cycle). We note that, with the original solution, multiple runs of the function was used and added up to roughly 1000ms for total generation time (See Fig. 1). With shorter SVG generation, only one run of the function was used and only took 16ms (See Fig. 2.).

Figure 1. Perf Logs with Complete SVG Generation original

Figure 2. Perf Logs with Shorter SVG Generation new

An improvement of generation time of around 40x to 50x is expected.

One could replicate these experiments by toggling WaveSim.Constants.generateVisibleOnly to control the generation range of the SVG, and toggling WaveSim.Constants.showPerfLogs to see the performance monitoring output.

samuelpswang avatar May 25 '24 00:05 samuelpswang

Nice. This looks good enough to implement on demand waveform generation while scrolling.

tomcl avatar May 25 '24 07:05 tomcl

Stage 2: Waveform Values Display Method

Summary

  • Changes were made to displayUInt32OnWave and displayBigIntOnWave to implement the updated view method.
  • Numbers were displayed in the following format:
    • No space for 1 copy: grey polygons, drawn manually, styling can be found in wavePolyfillStyle.
    • Space for 1 copy: a copy is displayed in the center of the waveform.
    • Space for 2 or more copies: two copies are displayed at either end of the waveform, padding between edge of waveform is automatically set to padding of a centered single-cycle waveform, but set to a constant if a single-cycle waveform can fit more than 2 copies.
  • Ability to display values in the old method was left out.

Quick Demo

New value display methods were tested on a simple counter circuit, see Fig. 1.

Figure 1. Counter Circuit circuit

Figures 2 to 4 offers different zoomed views of the updated value display method.

Figure 2. Low Zoom (18 cycles) zoom-0

Figure 3. Mid Zoom (12 cycles) zoom-1

Figure 4. High Zoom (3 cycles) zoom-2

samuelpswang avatar Jun 04 '24 05:06 samuelpswang

Stage 3: Scrollbar v1

Summary

  • Working scrollbar was achieved.
  • Changes to enable static scrollbar representation:
    • Fields were added to WaveSimModel to track scrollbar information.
    • Function generateScrollbarInfo was added to refreshWaveSim to update WaveSimModel when it is changed.
    • Function makeScrollbar was added to viewWaveSim to make React elements that will be inserted into the DOM.
  • Changes to enable dynamic scrollbar control:
    • New message type -- ScrollbarMouseMsg -- was added.
    • Function setClkCycleWithCounter was added to shift viewed cycles by a non-integer amount.
    • Function updateScrollbar updates the WaveSimModel after the scrollbar mouse message is issued.

Scrollbar Components

The scrollbar consisted of 4 major components, as shown in Fig. 1 below. The components are labeled in the figure:

  1. Side buttons: buttons with similar function like those on the top, one on each side.
  2. Scrollbar background: grey SVG rectangle that serves as the backdrop to the control thumb.
  3. Scrollbar thumb: white SVG rectangle that represents the clock cycle range we are currently viewing and also serves as the main control of the scrollbar.
  4. Thumb buffer zone: invisible SVG rectangles spanning a width of 32px before and after the thumb, used to catch mouse movements allowing the scrollbar to feel more smooth.

Uses and behaviours of these components will be discussed in later sections.

Figure 1. Anatomy of a Scrollbar

scrollbar-anatomy

Representation Mechanics

See the representation mechanics in action below in Movie 1. The representation mechanics can be described in the following three main points

  1. The scrollbar background initially represents 100 cycles.
  2. The scrollbar background will later represent "0 to the furthermost cycle the user has visited", and this can be achieved by dragging the thumb at the end or using the buttons.
  3. The scrollbar thumb approximates the location and percentage of the viewed range; and need not be too small such that it is impossible to drag. Therefore:
    • If the thumb width to represent the viewed range is calculated to be LESS THAN 5px, the width is set to 5px, and the centre of the thumb is set to the location of the current cycle within the represented range.
    • If the thumb width is calculated to be MORE THAN 5px, the width is used as such, and the centre of the thumb is set to the position of the centre of the viewed range within the represented range. I.e. it does not move if you go between cycles within the viewed range.

The function generateScrollbarInfo is just a template for translating viewed range information stored in WaveSimModel into the characteristics of the scrollbar. This can be later swapped out to accommodate more complicated representations.

Movie 1. Scrollbar Scaling Demo

https://github.com/user-attachments/assets/61ca2394-609c-47a2-8476-6d4c8bce5375

Control Mechanics

Control of the scrollbar operates under the these assumption and goals:

  1. The length of the viewed range will not change when you are scrolling.
  2. The thumb should follow the cursor as closely as possible when it is being dragged. We note that true drag is not achievable through MouseMove events, as if true drag is implemented, the mouse will not be moving in relation to the thumb.

The following mechanics was therefore devised:

  1. MouseMove events will give off the distance the mouse moved in pixels, which can then be translated into the number of cycles the thumb and the viewed range should move by.
  2. The number of cycles to move by will not be an integer, so instead the viewed range is moved as close to the given non-integer cycles as possible, the remaining cycles to be scrolled is stored in the ScrollbarCounter variable in WaveSimModel.
  3. Since the ScrollbarCounter is not cleared upon mouse release, and we have no way of knowing (specifically, if the mouse is NOT released upon the scrollbar SVG element): the remainder cycles, or "the difference between where the thumb should be if scrolling was fluid and where the thumb is currently at", may add up, and we will see the thumb slow down behind the cursor. This is problematic because if the cursor is sufficiently fast, it may run over the thumb and scrolling would stop, which is just bad user experience.
  4. To fix this, two buffer zones to the left and right of the thumb was added. If the cursor moved off the thumb and into the buffer zones, the viewed range will move forward/backwards faster, which allows the thumb to "catch up" to be under the cursor. If the mouse is dragged over the buffer zone directly, the viewed range will still move but the thumb will quickly catch up to the cursor and scrolling at a normal rate will be restored. We note that the scrolling in the buffer zone is uni-directional, i.e. you can only go right in the right buffer zone and left in the left buffer zone.

The combination of these mechanics has allowed the scrollbar to achieve a sufficiently comfortable scrolling experience. See Movie 2 for a demo of fast scrolling.

We further note that the function linearMouseToCycleTranslator translates mouse movement to number of cycles to move by, which can be swapped out to provide more complicated control mechanics.

Movie 2. Scrollbar Controlling Demo

https://github.com/user-attachments/assets/4957668c-e3b3-4e27-8fe5-a83b9ef06226

samuelpswang avatar Jul 29 '24 20:07 samuelpswang

OK - feedback

  1. This is a vast improvement on what we had before. great work.
  2. The mouse-move events need to be detected throughout the RHS - so that if cursor moves off the scrollbar dragging continues.
    • This can easily be implemented, but you need to use absolute mouse coordinates to control the drag, storing the offset between the mouse X position and the puck on mouse-down, calculating from this and the current mouse X coordinate the correct exact puck (and waveform) position.
    • Locking puck and waveforms to a cycle edge works fine as is and can easily still be done.
  3. Since the puck can be exactly positioned (modulo clock cycle quantisation) under the mouse the buffer zones etc are not needed, (Strictly speaking the puck position muts have the ame offset from the mouse it had on mouse-down, otehrwise you get an unpleasant puck movement on mouse down.
  4. All the above requires a float option in Model to determine whether a scroll drag is happenning, and what was the offset of teh puck from the cursor on button-down

tomcl avatar Aug 02 '24 20:08 tomcl

Stage 4: Scrollbar v2

NB: Development branch moved from samuelpswang/dev-#448 to samuelpswang/dev-#448-tmp, in order to use the rebasing work done by @tomcl.

Summary

  • Representation mechanics updates: when thumb size is calculated to be below 5px limit, WaveSimModel.StartCycle is used determine thumb position instead of WaveSimModel.CurrClkCycle as in v1. [14ec821]
  • Control mechanics updates: as suggested in previous comment, control is now based on viewport coordinates (clientX) instead of aggregating MouseMove event vectors (movementX) and adjusting in v1. Scroll is also tracked across whole right panel. Detailed explanation below. [24c3d02]
  • Scrollbar queue: to accommodate longer waveform generation time, a counter (WaveSimModel.ScrollbarQueueIsEmpty) to denote whether last generation request has completed was introduced. Detailed explanation below. [3bf323b]
  • Miscellaneous improvements:
    • Highlighted cycle flickering when moving: instead of using start and end of viewed cycles, previous current cycle or closest viewed cycle is used.
    • Weird behaviour when zoomed out at start of waveform: instead of using "max viewed cycle" as number of cycles represented, max of "max viewed cycle" and "twice the number of shown cycles" is used. Shown cycles are also removed from save data, in order to force background representation cycles to be recalculated everything a project is opened. Steps to reproduced issue and solution discussed in private emails between @samuelpswang and @tomcl. [7f18aaf]
    • Max simulation cycle increased: to 10k cycles, from 2k, from 1k. [14ec821, 8524161]

Discarded Features

  • Variable scroll: scrolling slower when shift key is pressed in order to scroll by 1 cycle in high zoom.
    • This was implemented: but breaks the constant offset relationship between thumb and cursor.
    • This was mitigated by: adjusting the scrollbar buttons to move view window by 1 cycle instead of shifting.

Deep Dive: Control Mechanics Updates

The new control mechanics can be described as the following points:

  1. When scrolling starts: offset between thumb position and cursor was recorded in WaveSimModel.ScrollbarTbOffset.
  2. During scrolling: attempts to move thumb to the same offset as initially recorded, which was done by adding the cursor position to the recorded offset. Note here that clientX was used instead of screenX as the latter gives absolute pixels AFTER scaling.
  3. When scrolling ends: clear offset.

This logic was implemented across the whole of the right panel, in order for scrolling to continue even if the cursor moved off of the scrollbar:

  1. Scrollbar thumb:
    • OnMouseDown = start scroll,
    • OnMouseMove = move only if in scroll,
    • OnMouseUp = end scroll only if in scroll.
  2. Right view panel:
    • OnMouseMove = move only if in scroll,
    • OnMouseUp = end scroll only if in scroll.

This meant that the following structure are now redundant and were removed:

  1. Buffer zones before and after scrollbar thumb were removed: because the new control mechanism will attempt to move the thumb as close to the cursor as possible.
  2. Counter to track incomplete cycles was removed: because "skid" between thumb and cursor will not stack up anymore, since absolute coordinates was used.

Deep Dive: Scrollbar Queue

During scrolling, mouse coordinate updates are small and frequent. This would allow smooth scrolling behaviour if waveform SVG generation is sufficiently fast. However when multiple waveforms are fully zoomed out (e.g. 50+ waves at 80+ cycles), the scrolling behaviour was observed to be much slower but catches up to the cursor eventually.

To mitigate this, a queueing system was devised in order to: not dispatch ScrollbarMouseMsg faster than they could be processed. This was done with the following logic:

  1. Counter tracking if a mouse message is being processed: WaveSimModel.ScrollbarQueueIsEmpty.
  2. When a mouse event is detected...
    • If there IS NOT a event being processed (queue is empty), we dispatch a message to update queue counter to be NOT empty and a message to update the scrollbar.
    • If there IS a event being processed (queue is NOT empty), the current mouse event is ignored.
  3. When a message to update the scrollbar is received...
    • It moves the thumb as close to the current cursor position as possible and clears the counter.
    • This can be seen on the screen as a jumping motion or like dropped frames.

While this cannot avoid the choppy motion when there are large number of waveforms present, it reduces resource usage which allowed for slightly better user experience.

samuelpswang avatar Aug 05 '24 00:08 samuelpswang

Better SVG generation, including:

  • generating only visible bits of SVG,
  • displaying just 2 copies of numeric values,
  • displaying grey polygons when there is no space,

was implemented in PR #456. A scrollbar was also added, as per the reach goal for #448 (this issue) and #332.

samuelpswang avatar Aug 05 '24 00:08 samuelpswang