Page 1 of 1

high performance ImageList all OS

Posted: Sat Feb 11, 2017 5:19 am
by Keya
This example which uses Microsoft's comctl32 ImageList api to create a list of icons (16x16 @ 32bit) takes 21ms on my machine for 1000 icons, and 2 seconds for 10000. However getting exponentially worse, it takes 54sec for 50,000 and 211sec for 100,000 icons.

Code: Select all

CreateImage(1, 16, 16, 32)
Time1 = ElapsedMilliseconds()
*imagelist = ImageList_Create_(16,16,#ILC_COLOR32,0,10)  
For i = 0 To 99999 
  ImageList_Add_(*imagelist, ImageID(1), 0)
Next i
Time2=ElapsedMilliseconds()
MessageRequester("Time", Str(Time2-Time1))
ImageList_Destroy_(*imagelist)
So I wrote my own, which does the the same 100,000 icons in ~28ms. It can be used for example as the basis for a custom list/combo/tree/animated/etc gadget.

The main benefit of using an ImageList is that you don't need a seperate Bitmap + open handle for every icon, yet every icon is still immediately ready for use at any time. Each icon is drawn onto 1 single image (or memory allocation) one after another like a linear set of tiles/subimages, so you're then only resource-limited, and finding the offset to any of them is as simple as Offset=(IconIndex*SizeOfIconBuffer).

Unlike comctl32's ImageList, which stores everything in one Bitmap which grows in pixel height, my ImgList doesn't use any drawing commands, just Copy/Free/Re/AllocateMemory. Comctl32's imagelist grows a Bitmap in height, my ImgList ReAllocatememory's.

It also only adds 1.1kb to the size of your PB app, going by win32 exe size. There is one exception - ImgList_Get() does use a couple of drawing commands, but you don't need to include that function as you can just use ImgList_Read() instead and provide your own existing CreateImage'd image.

However there are two main use cases for ImageLists - if you know a priori how many images you need to load you probably want to preallocate all memory up-front rather than repeatedly calling AllocateMemory. But if you don't know, or need the ability to also dynamically add and remove images, you probably do want each image allocated individually.
So mine efficiently supports both, which i simply refer to as Dynamic mode and NonDyn (pre-allocated) mode.

PROCEDURES OVERVIEW:

Code: Select all

;All functions return non-zero on success.
;ImgList_Create(width,height,depth,pitch,numimages=0,flags=0)
;  - Creates a new ImgList. Returns *ptr.IMGLIST
;    If numimages>0 the entire memoryspace is preallocated (NonDyn mode) 
;    If numimages=0 each image has its own MemoryAllocate() (Dynamic mode) Allows for ImgList_Remove.
;    All images must be the same width/height/depth, but there is no buffer overrun or security issue if not.
;    The memory cost of both modes is maximally efficient and basically the same, ie. numimages*imagebuffersize,
;    except Dynamic mode also has the tiny overhead of 1x integer-size pointer per image.
;    Flags can be set to #PB_Memory_NoClear to also get a minor speed boost by not zeroing the image memory.
;ImgList_Destroy(*ImgList.IMGLIST)
;  - Frees all images in the IMGLIST, and then frees the list itself.
;ImgList_Remove(*ImgList.IMGLIST, position)
;  - Removes/frees a single image from the list. (Only applicable in Dynamic mode where each image is individually Alloc'd)
;ImgList_Add(*ImgList.IMGLIST, position, *imgbuf, pitch, height)
;  - Add or overwrite an image in the list. 
;    In Dynamic mode -1 can be used to append a new image to the list and increment its \imgcnt by 1.
;    In NonDyn mode -1 cant be used as there is no appending; the position to overwrite must be specified, as its space is already preallocated.
;ImgList_Read(*ImgList.IMGLIST, position, *dstbuf)
;  - Read the buffer of a single image from the list. (Typically pointed at DrawingBuffer())
;ImgList_Get(*ImgList.IMGLIST, position)
;  - Get a single image from the list as a CreateImage'd handle. The only function that uses drawing commands, you may prefer just to use ImgList_Read instead.
IMAGELIST.PBI

Code: Select all

#IMGLIST_MAXITEMS = 1048575 ;Max 1,048,575 items = maximum 1GB worth of 16x16 32bit images (seems a reasonable ceiling)
#IMGLIST_FWDBUF   = 256     ;(for NonDyn) allocate a small (#IMGLIST_FWDBUF * SizeOf(Integer)) buffer so we dont have to ReAlloc with every ImgList_Add, only every #IMGLIST_FWDBUF'th

Structure pbuf
  pbuf.i[0]
EndStructure

Structure IMGLIST
  *IMG.pbuf
  imgcnt.l
  width.l
  height.l
  depth.l
  pitch.l
  bufsize.l
  flags.l    ;0 or #PB_Memory_NoClear
  dynamic.l
EndStructure 

Procedure ImgList_Create(width,height,depth,pitch,numimages=0,flags=0) ;Success=non-zero (ptr to IMGLIST)
  If width > 0 And height > 0 And depth > 0 And pitch > 0
    Protected *ImgList.IMGLIST = AllocateMemory(SizeOf(IMGLIST))
    If *ImgList
      *ImgList\width=width
      *ImgList\height=height
      *ImgList\depth=depth
      *ImgList\pitch=pitch
      *ImgList\imgcnt=numimages
      *ImgList\flags=flags
      *ImgList\bufsize = pitch * height
      *ImgList\IMG = AllocateMemory(SizeOf(Integer))
      If numimages > 0
        *ImgList\IMG\pbuf[0] = AllocateMemory(*ImgList\bufsize * numimages, flags)
      Else
        *ImgList\dynamic = 1
      EndIf
      ProcedureReturn *ImgList
    EndIf
  EndIf
EndProcedure


Procedure ImgList_Destroy(*ImgList.IMGLIST) ;success=1
  If *ImgList
    If *ImgList\imgcnt
      If *ImgList\dynamic
        For i = 0 To *ImgList\imgcnt-1
          If *ImgList\IMG\pbuf[i]
            FreeMemory(*ImgList\IMG\pbuf[i])
          EndIf
        Next i
      Else
        FreeMemory(*ImgList\IMG\pbuf[0])
      EndIf
    EndIf
    If *ImgList\IMG: FreeMemory(*ImgList\IMG): EndIf
    FreeMemory(*ImgList)
    ProcedureReturn 1
  EndIf
EndProcedure


Procedure ImgList_Remove(*ImgList.IMGLIST, position) ;success=1. Only applicable with Dynamic mode (where each image is individually Alloc'd)
  If *ImgList
    If *ImgList\dynamic And *ImgList\IMG\pbuf[position]
      FreeMemory(*ImgList\IMG\pbuf[position])
      *ImgList\IMG\pbuf[position]=0
      ProcedureReturn 1
    EndIf
  EndIf
EndProcedure


Procedure ImgList_Add(*ImgList.IMGLIST, position, *imgbuf, pitch, height) ;success=1
  Protected rc=0, srcbuflen=pitch*height
  If Not *ImgList Or Not *imgbuf
  ElseIf srcbuflen > *ImgList\bufsize Or position < -1 Or position => *ImgList\imgcnt
  Else
    If *ImgList\dynamic
      If position = -1 ;Add New (dyn)
        If *ImgList\imgcnt = #IMGLIST_MAXITEMS: ProcedureReturn 0: EndIf
        If Not (*ImgList\imgcnt % #IMGLIST_FWDBUF)
          *ImgList\IMG = ReAllocateMemory(*ImgList\IMG, (*ImgList\imgcnt + #IMGLIST_FWDBUF) * SizeOf(Integer))
        EndIf
        *ImgList\IMG\pbuf[*ImgList\imgcnt] = AllocateMemory(*ImgList\bufsize, *ImgList\flags)
        CopyMemory(*imgbuf, *ImgList\IMG\pbuf[*ImgList\imgcnt], *ImgList\bufsize)
        *ImgList\imgcnt+1
      Else ;Overwrite (dyn)
        CopyMemory(*imgbuf, *ImgList\IMG\pbuf[position], *ImgList\bufsize)
      EndIf
      rc=1
    Else  ;Overwrite (non-dyn)
      If position => 0
        CopyMemory(*imgbuf, *ImgList\IMG\pbuf[0] + (position * *ImgList\bufsize), *ImgList\bufsize)
        rc=1
      EndIf
    EndIf
  EndIf
  ProcedureReturn rc
EndProcedure


Procedure ImgList_Read(*ImgList.IMGLIST, position, *dstbuf) ;success=1
  Protected rc=0
  If *ImgList
    If position => 0 And position < *ImgList\imgcnt
      If *ImgList\dynamic
        If *ImgList\IMG\pbuf[position] And *dstbuf        
          CopyMemory(*ImgList\IMG\pbuf[position], *dstbuf, *ImgList\bufsize)
          rc=1
        EndIf
      Else
        CopyMemory(*ImgList\IMG\pbuf[0] + (position * *ImgList\bufsize), *dstbuf, *ImgList\bufsize)
        rc=1
      EndIf
    EndIf
  EndIf
  ProcedureReturn rc
EndProcedure


;this is the only function that uses drawing commands. You may just want to use ImgList_Read instead.
Procedure ImgList_Get(*ImgList.IMGLIST, position) ;success=non-zero (hImg). Callers responsibility to FreeImage
  If *ImgList
    hImg = CreateImage(#PB_Any, *ImgList\width, *ImgList\height, *ImgList\depth)
    If StartDrawing(ImageOutput(hImg))
      ImgList_Read(*ImgList.IMGLIST, position, DrawingBuffer())
      StopDrawing()
      ProcedureReturn hImg
    EndIf
  EndIf
EndProcedure

Re: high performance ImageList all OS

Posted: Sat Feb 11, 2017 5:23 am
by Keya
Example
Here is a (very poor) "custom list gadget", just a scrollbar and five image gadgets so it doesn't get much rougher, but adequate enough to show the general idea. It makes 100,000 calls to DrawText() (drawing the index# on each image) so comment that call out to properly speed-test.

Code: Select all

IncludeFile("ImageList.pbi")

CompilerIf #PB_Compiler_Debugger
  CompilerError "Turn debugger off for timing"
CompilerEndIf

Enumeration
  #Img0
  #Img1
  #Img2
  #Img3
  #Img4
  #Dlg1
  #ScrollBar
EndEnumeration

Global gimagecnt=99999
Global width=50    ;typically 16x16 but wider here to show large numbers
Global height=16
Global depth=32 ;24
Global flags=#PB_Memory_NoClear
Global Dim hImg(4)
Global *ImgList.IMGLIST ;no need to use globals as they're dynamically allocated, but easier for this demo


Procedure OpenDlg1(x = 0, y = 0, width = 196, height = 284)
  OpenWindow(#Dlg1, x, y, width, height, "Test", #PB_Window_SystemMenu | #PB_Window_MinimizeGadget | #PB_Window_ScreenCentered)
  ScrollBarGadget(#ScrollBar, 140, 32, 24, 228, 0, 1, 0, #PB_ScrollBar_Vertical)
  SetGadgetAttribute(#ScrollBar, #PB_ScrollBar_Maximum, gimagecnt-4)
  ImageGadget(#Img0, 80, 36, 50, 16, 0)
  ImageGadget(#Img1, 80, 56, 50, 16, 0)
  ImageGadget(#Img2, 80, 76, 50, 16, 0)
  ImageGadget(#Img3, 80, 96, 50, 16, 0)
  ImageGadget(#Img4, 80, 116, 50, 16, 0)
EndProcedure


Procedure ScrollBar()
  pos = GetGadgetState(#ScrollBar)
  For i = #Img0 To #Img4
    If StartDrawing(ImageOutput(hImg(i)))
      rc=ImgList_Read(*ImgList, pos+i, DrawingBuffer())
      StopDrawing()
      If rc: SetGadgetState(i, ImageID(hImg(i))): EndIf
    EndIf
  Next i
EndProcedure

Procedure CreateAndLoadImageList(*DrawingBuffer, pitch)
  *ImgList.IMGLIST = ImgList_Create(width,height,depth,pitch, gimagecnt+1, flags)
  For i = 0 To gimagecnt
    DrawText(0,0,Str(i),$FFFFFF, RGB(Random(255,0),Random(255,0),$80))  ;Comment-out to see how fast it really is
    ImgList_Add(*ImgList, i, *DrawingBuffer, pitch, height)
  Next i
EndProcedure

Procedure CreateAndLoadImageList_Dynamic(*DrawingBuffer, pitch)
  *ImgList.IMGLIST = ImgList_Create(width,height,depth,pitch, 0, flags)
  For i = 0 To gimagecnt
    DrawText(0,0,Str(i),$FFFFFF, RGB(Random(255,0),Random(255,0),$80))  ;Comment-out to see how fast it really is
    ImgList_Add(*ImgList, -1, *DrawingBuffer, pitch, height)
  Next i
EndProcedure


Procedure LoadForm()
  For i = 0 To 4: hImg(i) = CreateImage(#PB_Any, width,height,depth): Next
  OpenDlg1()
  SetWindowTitle(#Dlg1,"Loading "+Str(gimagecnt)+"...")
  hImg = CreateImage(#PB_Any, width,height,depth)
  If StartDrawing(ImageOutput(hImg))
    pitch = DrawingBufferPitch()
    *DrawingBuffer = DrawingBuffer()  
    
    Time1=ElapsedMilliseconds()
    CreateAndLoadImageList(*DrawingBuffer, pitch)           ;1) NonDyn (fully pre-allocated)
    ;CreateAndLoadImageList_Dynamic(*DrawingBuffer, pitch)  ;or, 2) Dynamic (individually allocated)
    Time2=ElapsedMilliseconds()  
    
  EndIf
  StopDrawing()
  FreeImage(hImg)
  SetWindowTitle(#Dlg1, Str(Time2-Time1) + "ms")
  ScrollBar()
  BindEvent(#PB_Event_Gadget, @ScrollBar(), #Dlg1, #Scrollbar)
EndProcedure


LoadForm()
Repeat
Until WaitWindowEvent() = #PB_Event_CloseWindow
ImgList_Destroy(*ImgList)

Re: high performance ImageList all OS

Posted: Sat Feb 11, 2017 5:57 pm
by minimy
Hi, I test in W10 and work fine and quickly..
Thanks for share!

Re: high performance ImageList all OS

Posted: Sun Feb 12, 2017 1:13 am
by Dude
Thanks for sharing, Keya. I know this has been a big bother for you. :)

Re: high performance ImageList all OS

Posted: Wed Feb 15, 2017 12:16 pm
by Kwai chang caine
Works well (2284 ms W7 x86 v5.60B3)
Thanks for sharing 8)