File: imgblockstore.pas

package info (click to toggle)
ddrescueview 0.4.5-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,212 kB
  • sloc: pascal: 2,770; makefile: 8
file content (504 lines) | stat: -rw-r--r-- 19,969 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
(*
   ImgBlockStore.pas - Image Block Storage unit

   Copyright (C) 2013 - 2022 Martin Bittermann (martinbittermann@gmx.de)

   This file is part of ddrescueview.

   ddrescueview is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   ddrescueview is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with ddrescueview.  If not, see <http://www.gnu.org/licenses/>.
*)

// {$DEFINE VerboseProfiler}

unit ImgBlockStore;

interface

uses
  Classes, SysUtils, Parser, Shared;

type

  TZoom = record
    SectOffset : int64; // imgBlocks start after this # of device sectors (zooming)
    SectCount : int64;  // number of device sectors to show in imgBlocks (zooming)
  end;
  { TImgBlockStore }
  TImgBlockStore = class(TObservablePersistent, IFPObserver)
  private
    FImgBlocks: Array of Longword; // lower 24 bits : color, upper 8 bits: status mask
    FParser : TMapParser; // parser / map source to use
    FNumBlocks : Longint;  // number of blocks requested
    FSectSize : Int64; // device sector size
    FZoom : TZoom;
    FSectInBlock : Int64; // device sectors per imgBlock
    FByteInBlock : Int64; // # of bytes in every imgBlock, multiple of FDevBSize
    FOwner : TObject;
    FNoZoom : Boolean;    // prevent zooming. Always show full device
    FLockOnCurPos : Boolean; // center around current position if possible but allow zoom
    FBlockstatusMasks : array[0..255] of Longword; // LUT, maps log entry statuses to Block Status masks
    procedure assignParser(source : TMapParser);
    procedure buildBlocks;
    function BlockStatusToMask(status: char) : Longword;
    function ColorizeBlockMask(mask: Longint) : Longint;
    function getBlock(i: Longint): Longword;
    procedure sanitizeZoom();
    function binSearchStartEntry() : integer;
    function binSearchEndEntry() : integer;
    procedure setDevBSize(size : Int64);
    procedure SetLockOnCurPos(lock: Boolean);
    procedure setNumBlocks(num : Longint);
    procedure setSectOffset(offset : Int64);
    procedure setSectCount(count : Int64);
  public
    constructor Create(AOwner : TObject); virtual;
    destructor Destroy; override;
    property Blocks[i:Longint] : Longword read getBlock; default;
    property Parser : TMapParser read FParser write assignParser;
    property NumBlocks : Longint read FNumBlocks write setNumBlocks;
    property SectSize : Int64 read FSectSize write setDevBSize;
    property SectOffset : Int64 read FZoom.SectOffset write setSectOffset;
    property SectCount : Int64 read FZoom.SectCount write setSectCount;
    property ByteInBlock : Int64 read FByteInBlock;
    property SectInBlock : Int64 read FSectInBlock;
    property NoZoom : Boolean read FNoZoom write FNoZoom;
    property LockOnCurPos : Boolean read FLockOnCurPos write SetLockOnCurPos;
    function getOffsetFromBlock(block : Int64) : Int64;
    function getBlockFromOffset(offset : Int64) : Int64;
    function activeBlock() : Int64;
    procedure setZoom(Offset, Count: Int64);
    procedure zoomToCenter(block: Longint; factor: double);
    procedure zoomBlockToBlock(srcBlock, destBlock: Longint; factor: double);
    procedure zoomInRange(block1, block2: Longint);
    Procedure FPOObservedChanged(ASender : TObject; //for the Observer role
      Operation : TFPObservedOperation; Data : Pointer);
  end;

implementation
uses GUI, Math;

// creates the ImgBlockStore and sets up listening for the owner.
constructor TImgBlockStore.Create(AOwner : TObject);
var c : byte;
begin
  inherited Create;
  FSectSize := DEF_BSIZE;
  FByteInBlock := DEF_BSIZE;
  FSectInBlock := 1;
  FOwner := AOwner;
  // Precompute the Block Status Masks LUT
  for c:= 0 to 255 do FBlockstatusMasks[c]:=BlockStatusToMask(char(c));
  // listen for requests from the owner
  if (AOwner<>nil) and (AOwner is IAttachableObserved) then
    (AOwner as IAttachableObserved).AttachObserver(self);
end;

destructor TImgBlockStore.Destroy;
begin
  SetLength(FImgBlocks, 0);
  if FParser<>nil then FParser.DetachObserver(self);
  if (FOwner<>nil) and (FOwner is IAttachableObserved) then
    (FOwner as IAttachableObserved).DetachObserver(self);
  inherited;
end;

procedure TImgBlockStore.FPOObservedChanged(ASender: TObject;
  Operation: TFPObservedOperation; Data: Pointer);
begin
  if ASender = FParser then begin // parser did its thing ...
    if Operation in [ooAddItem, ooDeleteItem] then begin // new or closed file!
      if Operation = ooAddItem then FSectSize:=FParser.rescueStatus.suggestedBlockSize;
      FZoom.SectCount:=0; // reset zoom
    end;
  end;
  sanitizeZoom();
  buildBlocks;
  // log the update to mainform textbox
  //MainForm.DbgLog.Lines.Add('ImgBlockStore update: ' + BoolToStr(FNoZoom, 'Zoom', 'Grid'));
  FPONotifyObservers(self, ooChange, nil);
end;

// Assign a MapParser to the img Blocks.
// Assign another parser before freeing the old parser from the outside!
procedure TImgBlockStore.assignParser(source : TMapParser);
begin
  if source=nil then raise Exception.Create('imgBlockStore: Cannot assign nil parser!');
  if FParser<>nil then FParser.DetachObserver(self); // detach old source
  FParser:=source;
  FSectSize:=FParser.rescueStatus.suggestedBlockSize;
  FZoom.SectCount:=0; // reset zoom
  FParser.AttachObserver(self);
  buildBlocks;
  FPONotifyObservers(self, ooChange, nil);
end;

function TImgBlockStore.getBlock(i: Longint): Longword;
begin
  getBlock:=FImgBlocks[i];
end;

// Gets the Offset of a given imgBlock on the rescue device, in bytes.
function TImgBlockStore.getOffsetFromBlock(block: Int64): Int64;
begin
  getOffsetFromBlock:=FZoom.SectOffset*FSectSize + FByteInBlock*block;
end;

// Gets the block index that belongs to a given offset on the device.
// Aside from round, this is the opposite of getOffsetFromBlock.
// Can be negative or greater than numBlocks-1 when zoomed.
function TImgBlockStore.getBlockFromOffset(offset : Int64) : Int64;
var localOffset : Int64;
begin
  localOffset := offset - FZoom.SectOffset*FSectSize;
  if FByteInBlock > 0 then result:=(localOffset div FByteInBlock) -
    Int64((localOffset mod FByteInBlock <> 0) and (localOffset < 0))
  else result:=0;
end;

// returns imgblock index of current position. May be out of range, see above.
function TImgBlockStore.activeBlock() : Int64;
begin
  if Assigned(FParser) then
    result:=getBlockFromOffset(FParser.rescueStatus.pos)
  else result:=0;
end;

// clean up the zoom settings:
// * set zoom to span whole device if not already set, or FNoZoom is true
// * limit zoom section to device size
// * recalculate FSectInBlock and FByteInBlock
procedure TImgBlockStore.sanitizeZoom();
var
  deviceSectors: Int64;
begin
  if (not Assigned(FParser)) or (FNumBlocks = 0) or (FSectSize = 0) then exit;
  with FParser do begin
    deviceSectors:=rescueStatus.devicesize div FSectSize;
    // first, if FLockOnCurPos is true, keep the current position locked in the center:
    if FLockOnCurPos and not FNoZoom then
      FZoom.SectOffset:=(rescueStatus.pos div FSectSize)-(FZoom.SectCount div 2);

    if (FZoom.SectCount=0) or FNoZoom then begin
      FZoom.SectOffset:=0;
      FZoom.SectCount:=deviceSectors;
    end;

    FSectInBlock := FZoom.SectCount div FNumBlocks + Int64((FZoom.SectCount mod FNumBlocks)<>0);
    FByteInBlock := FSectInBlock * FSectSize;
    FZoom.SectCount := Max(0, Min(FSectInBlock * FNumBlocks, deviceSectors - FZoom.SectOffset));
    // now if the user zoomed in all the way to the end of the device, some
    // (or many!) blocks at the end may be unused -> fix this by decrementing offset:
    FZoom.SectOffset -= min(FZoom.SectOffset, max(0,
      FSectInBlock * FNumBlocks - deviceSectors + FZoom.SectOffset));
    // now try to set larger sectCount again...
    FZoom.SectCount := Max(0, Min(FSectInBlock * FNumBlocks, deviceSectors - FZoom.SectOffset));
  end;
end;

// Binary Search in the Parser's Log for the starting entry inside the BlockStore's Zoom
function TImgBlockStore.binSearchStartEntry(): integer;
var
  chunkStart, chunkEnd, chunkSize, chunkMid : integer;
  searchOffset : Int64;
begin
  searchOffset := FZoom.SectOffset*FSectSize;
  chunkStart := 0;
  chunkEnd := Length(FParser.map)-1;
  chunkSize := chunkEnd-chunkStart+1;
  while chunkSize > 1 do begin
    chunkMid := chunkStart + (chunkSize div 2);
    if searchOffset < (FParser.map[chunkMid-1].offset+FParser.map[chunkMid-1].length) then chunkEnd := chunkMid-1
    else chunkStart := chunkMid;
    chunkSize := chunkEnd-chunkStart+1;
  end;
  result:=chunkStart;
end;

// Binary Search in the Parser's Log for the last entry making up the BlockStore's Zoom
function TImgBlockStore.binSearchEndEntry(): integer;
var
  chunkStart, chunkEnd, chunkSize, chunkMid : integer;
  searchOffset : Int64;
begin
  searchOffset := (FZoom.SectOffset+FZoom.SectCount)*FSectSize;
  chunkStart := 0;
  chunkEnd := Length(FParser.map)-1;
  chunkSize := chunkEnd-chunkStart+1;
  while chunkSize > 1 do begin
    chunkMid := chunkStart + (chunkSize div 2);
    if searchOffset <= FParser.map[chunkMid].offset then chunkEnd := chunkMid-1
    else chunkStart := chunkMid;
    chunkSize := chunkEnd-chunkStart+1;
  end;
  result:=chunkEnd;
end;

(* Calculate image blocks from map for fast drawing.
   Blocks will be stored in the imgBlocks[] array.
   Blocks contain type information and a color derived from the mix of types.
   Each block spans a multiple of the device sector size.
   This can lead to some unused blocks at the end of the grid, especially with
   small rescue devices, high number of imgBlocks or big sector size *)
procedure TImgBlockStore.buildBlocks;
var
  mapLen, mapBlock, imgBlock : integer;
  imgStartBlock, imgEndBlock : Int64;
  blockStatusMask : Longword;
  {$IFDEF VerboseProfiler}
  lTimeStart : TDateTime;
  {$ENDIF}
begin
  if not Assigned(FParser) then mapLen := 0
  else mapLen := Length(FParser.map);

  // adjust block array to requested size (FNumBlocks)
  if Length(FImgBlocks) <> FNumBlocks then SetLength(FImgBlocks, FNumBlocks);

  if FNumBlocks = 0 then exit; // cannot proceed this way

  if mapLen = 0 then begin // empty map (can have one of several reasons)
    FZoom.SectOffset:=0;
    FZoom.SectCount:=0; // reset zoom
    if (not Assigned(FParser)) or (not FParser.hasFile) then begin // no file or parser
      FillDword(FImgBlocks[0], Length(FImgBlocks), COLOR_NON_TRIED);
      exit;
    end;
  end;

  {$IFDEF VerboseProfiler}
  lTimeStart := Now();
  {$ENDIF}

  FillDword(FImgBlocks[0], Length(FImgBlocks), $00000000); //zero-out all blocks

  with FParser do begin
    // if zoom not yet set, or zoom is disabled, do sanitization
    if (FZoom.SectCount=0) or FNoZoom then sanitizeZoom();
    // iterate over map and collect block info
    for mapBlock := binSearchStartEntry() to binSearchEndEntry() do begin
      // process img blocks that intersect current map entry
      imgStartBlock := (map[mapBlock].offset-FZoom.SectOffset*FSectSize) div FByteInBlock;
      imgEndBlock := (map[mapBlock].offset+map[mapBlock].length-FZoom.SectOffset*FSectSize-1) div FByteInBlock;
      blockStatusMask := FBlockstatusMasks[ord(map[mapBlock].status)];
      for imgBlock := Max(0,imgStartBlock) to Min(FNumBlocks-1, imgEndBlock) do begin
        FImgBlocks[imgBlock]:=FImgBlocks[imgBlock] or blockStatusMask;
      end;
    end;

    // mark active block, i.e. block of current input read position
    if (rescueStatus.pos >= FZoom.SectOffset*FSectSize) and
       (rescueStatus.pos < (FZoom.SectOffset+FZoom.SectCount)*FSectSize) then begin
      imgBlock := (rescueStatus.pos-FZoom.SectOffset*FSectSize) div FByteInBlock;
      FImgBlocks[imgBlock]:=FImgBlocks[imgBlock] or MASK_ACTIVE;
    end;

    // go through blocks and build colors
    for imgBlock := 0 to FNumBlocks-1 do begin
      FImgBlocks[imgBlock]:=ColorizeBlockMask(FImgBlocks[imgBlock]);
    end;
  end;
  {$IFDEF VerboseProfiler}
  MainForm.DbgLog.Lines.Add(Format('[TImgBlockStore.buildBlocks] Duration: %d ms', [DateTimeToMilliseconds(Now() - lTimeStart)]));
  {$ENDIF}
end;

(* computes a color for a block, depending on whick block types exist in
   this block. The percentage of sectors of a specific block type has no influence.
   The weight of a color (block type) can be set by the user, but is at default:
   (least) non-tried, finished, non-trimmed, non-scraped, bad (highest)
   Each of these types influences the block color depending on its weight.
   This way, small local quantities of important (e.g. bad) blocks are easy
   to notice over less important (rescued, non-tried) ones.
   TODO: Optimize this for speed! A 6bit lookup table would do. *)
function TImgBlockStore.ColorizeBlockMask(mask: Longint) : Longint;
var i, b, g, r, totalweight, statusPresent : Longint;
begin
  b:=0; g:=0; r:=0; totalweight:=0;
  if not LongBool(mask and MASK_ALL_STATUSES) then begin
    result := mask or COLOR_UNDEFINED;
    exit;
  end;
  for i:= 0 to Length(MASKS)-1 do begin
    statusPresent:=Longint((mask and MASKS[i]) <> 0);
    b := b + statusPresent*WEIGHTS[i]^*((COLORS[i]^ and MASK_B) shr 16);
    g := g + statusPresent*WEIGHTS[i]^*((COLORS[i]^ and MASK_G) shr 8);
    r := r + statusPresent*WEIGHTS[i]^* (COLORS[i]^ and MASK_R);
    inc(totalweight, statusPresent*WEIGHTS[i]^);
  end;
  b := b div totalweight;
  g := g div totalweight;
  r := r div totalweight;
  result := mask or (b shl 16) or (g shl 8) or r;
end;

(* sets the device sector size and tries to maintain zoom as much as possible *)
procedure TImgBlockStore.setDevBSize(size: Int64);
var newOffset : Int64;
begin
  // do not allow setting the sector size without a Parser
  if (not Assigned(FParser)) then begin
    MainForm.DbgLog.Lines.Add('Cannot set device sector size: no data source!');
    MainForm.DSClick(self); // revert menu back to previous value
    exit;
  end;
  // catch invalid device sizes, but always allow default
  // sector size, in case of abnormal device size
  if (size <> DEF_BSIZE) and
    ((size <= 0) or (FParser.rescueStatus.devicesize mod size <> 0)) then begin
    MainForm.DbgLog.Lines.Add('Cannot set device sector size: ' +
      inttostr(size) + ' does not evenly divide device size.');
    MainForm.DSClick(self); // revert menu back to previous value
    exit;
  end;
  if (size = FSectSize) then exit; // nothing to do, exit

  newOffset := (FZoom.SectOffset*FSectSize) div size;
  FZoom.SectCount := ((FZoom.SectOffset+FZoom.SectCount)*FSectSize) div size - newOffset;
  FZoom.SectOffset := newOffset;
  FSectSize := size;
  sanitizeZoom();
  buildBlocks;
  FPONotifyObservers(self, ooChange, nil);
end;

procedure TImgBlockStore.SetLockOnCurPos(lock: Boolean);
begin
  FLockOnCurPos:=lock;
  if FLockOnCurPos then begin
    sanitizeZoom();
    buildBlocks;
    FPONotifyObservers(self, ooChange, nil);
  end;
end;

procedure TImgBlockStore.setNumBlocks(num: Longint);
begin
  FNumBlocks:=num;
  sanitizeZoom();
  buildBlocks;
  FPONotifyObservers(self, ooChange, nil);
end;

// shift the zoomed section by changing the offset
procedure TImgBlockStore.setSectOffset(offset: Int64);
begin
  FZoom.SectOffset:=offset;
  sanitizeZoom();
end;

// resize zoomed section
procedure TImgBlockStore.setSectCount(count: Int64);
begin
  FZoom.SectCount:=count;
  sanitizeZoom();
end;

//set zoomed section
procedure TImgBlockStore.setZoom(Offset, Count: Int64);
begin
  FZoom.SectOffset:=Offset;
  FZoom.SectCount:=Count;
  sanitizeZoom();
end;

// returns a bit mask for each known block type
function TImgBlockStore.BlockStatusToMask(status: char) : Longword;
begin
  case status of
    '?': result := MASK_NON_TRIED;
    '+': result := MASK_FINISHED;
    '*': result := MASK_NON_TRIMMED;
    '/': result := MASK_NON_SCRAPED;
    '-': result := MASK_BAD_SECT;
    'd': result := MASK_OUTSIDE_DOMAIN;
  else
    result := 0;
  end;
end;

// zoom in or out by some factor, centered around a given imgBlock
// factor <1 zooms in, factor >1 zooms out.
procedure TImgBlockStore.zoomToCenter(block: Longint; factor: double);
var
  zoomCenter, zoomLength, deviceSectors : Int64; // all values in sectors
begin
  if not Assigned(FParser) then exit;
  if not FParser.hasFile() then exit;
  zoomCenter:=getOffsetFromBlock(block) div FSectSize;
  deviceSectors:=FParser.rescueStatus.devicesize div FSectSize;
  if factor < 1.0 then begin // zoom in
    // if already at 1 sector per block -> dont zoom further
    if FSectInBlock=1 then exit;
    zoomLength:=Max(FnumBlocks, // not further than 1 sector/block
      Min(FZoom.SectCount-FnumBlocks, // reduce window by at least 1 sector/block
      FZoom.SectCount * Round(128*factor) div 128));
  end else begin // zoom out
    zoomLength:=Min(deviceSectors, FZoom.SectCount * Round(128*factor) div 128);
  end;
  FZoom.SectCount:=zoomLength;
  FZoom.SectOffset:=Max(0, Min(zoomCenter-(zoomLength div 2), deviceSectors-zoomLength));
  sanitizeZoom();
  buildBlocks;
  FPONotifyObservers(self, ooChange, nil);
end;

// Zooms the view in or out. After the zoom, the device position of srcBlock
// will be at destBlock.
procedure TImgBlockStore.zoomBlockToBlock(srcBlock, destBlock: Longint; factor: double);
var
  zoomLength, deviceSectors, srcBlockSect, newSectInBlock : Int64; // all values in sectors
begin
  if not Assigned(FParser) then exit;
  if not FParser.hasFile() then exit;

  deviceSectors:=FParser.rescueStatus.devicesize div FSectSize;
  zoomLength := Round(FZoom.SectCount*factor); // desired new window size
  if factor < 1.0 then begin // zooming in
    // if already at 1 sector/block -> do nothing
    if FSectInBlock = 1 then exit;
    // reduce window by at least 1 sector/block (needed for rounding reasons)
    zoomLength := Min(FZoom.SectCount-FnumBlocks, zoomLength);
  end;
  zoomLength := Max(FnumBlocks, zoomLength); // not further in than 1 sector/block
  zoomLength := Min(deviceSectors, zoomLength); // not further out than whole device
  srcBlockSect := getOffsetFromBlock(srcBlock) div FSectSize + FSectInBlock div 2;
  newSectInBlock := zoomLength div FNumBlocks + Int64((zoomLength mod FNumBlocks)<>0);
  FZoom.SectOffset := srcBlockSect - (newSectInBlock*destBlock + newSectInBlock div 2);
  FZoom.SectCount := zoomLength;
  sanitizeZoom();
  buildBlocks;
  FPONotifyObservers(self, ooChange, nil);
end;

// zoom into an area bounded by two blocks. Assumes block1 <= block2.
procedure TImgBlockStore.zoomInRange(block1, block2: Longint);
var
  zoomOffset, zoomLength, deviceSectors : Int64; // all values in sectors
begin
  if not Assigned(FParser) then exit;
  if not FParser.hasFile() then exit;
  if FSectInBlock=1 then exit; // if at 1 sector per block, dont zoom further
  if block1 > block2 then exit; // need block1 <= block2
  zoomOffset:=getOffsetFromBlock(block1) div FSectSize;
  zoomLength:=getOffsetFromBlock(block2) div FSectSize + FSectInBlock - zoomOffset;
  deviceSectors:=FParser.rescueStatus.devicesize div FSectSize;
  FZoom.SectCount:=zoomLength;
  FZoom.SectOffset:=Max(0, Min(zoomOffset, deviceSectors-zoomLength));
  sanitizeZoom();
  buildBlocks;
  FPONotifyObservers(self, ooChange, nil);
end;

end.