source: branches/RWT/modules/vci_mem_cache/caba/source/include/mem_cache_directory.h @ 477

Last change on this file since 477 was 477, checked in by lgarcia, 11 years ago

Reintroducing RWT branch merging the last modifications of the
trunk (CLACK channel)
WARNING: bugs remaining (with 1c16p and small caches (L2:16*16; L1:4*4))

File size: 23.8 KB
Line 
1#ifndef SOCLIB_CABA_MEM_CACHE_DIRECTORY_H
2#define SOCLIB_CABA_MEM_CACHE_DIRECTORY_H
3
4#include <inttypes.h>
5#include <systemc>
6#include <cassert>
7#include "arithmetics.h"
8
9// !!!
10// The L1_MULTI_CACHE mechanism does no longer work with the new pktid encoding
11// of TSAR. Turning the define below to a non null value will cause the memcache
12// to behave in an unpredicted way.
13// TODO Either remove the mechanism from the mem cache or update its behaviour.
14
15#define L1_MULTI_CACHE 0
16
17//#define RANDOM_EVICTION
18
19namespace soclib { namespace caba {
20
21  using namespace sc_core;
22
23  ////////////////////////////////////////////////////////////////////////
24  //                    A LRU entry
25  ////////////////////////////////////////////////////////////////////////
26  class LruEntry {
27
28    public:
29
30      bool recent;           
31
32      void init()
33      {
34        recent=false;
35      }
36
37  }; // end class LruEntry
38
39  ////////////////////////////////////////////////////////////////////////
40  //                    An Owner
41  ////////////////////////////////////////////////////////////////////////
42  class Owner{
43   
44    public:
45    // Fields
46      bool      inst;       // Is the owner an ICache ?
47      size_t    srcid;      // The SRCID of the owner
48#if L1_MULTI_CACHE
49      size_t    cache_id;   // In multi_cache configuration
50#endif
51
52    ////////////////////////
53    // Constructors
54    ////////////////////////
55      Owner(bool   i_inst
56            ,size_t i_srcid
57#if L1_MULTI_CACHE
58            ,size_t i_cache_id
59#endif
60            ){
61        inst    = i_inst;
62        srcid   = i_srcid;
63#if L1_MULTI_CACHE
64        cache_id= i_cache_id;
65#endif
66      }
67
68      Owner(const Owner &a){
69        inst    = a.inst;
70        srcid   = a.srcid;
71#if L1_MULTI_CACHE
72        cache_id= a.cache_id;
73#endif
74      }
75
76      Owner(){
77        inst    = false;
78        srcid   = 0;
79#if L1_MULTI_CACHE
80        cache_id= 0;
81#endif
82      }
83      // end constructors
84
85  }; // end class Owner
86
87
88  ////////////////////////////////////////////////////////////////////////
89  //                    A directory entry                               
90  ////////////////////////////////////////////////////////////////////////
91  class DirectoryEntry {
92
93    typedef uint32_t tag_t;
94
95    public:
96
97    bool    valid;                  // entry valid
98    bool    cache_coherent;         // WB or WT policy
99    bool    is_cnt;                 // directory entry is in counter mode
100    bool    dirty;                  // entry dirty
101    bool    lock;                   // entry locked
102    tag_t   tag;                    // tag of the entry
103    size_t  count;                  // number of copies
104    Owner   owner;                  // an owner of the line
105    size_t  ptr;                    // pointer to the next owner
106
107    DirectoryEntry()
108    {
109      valid         = false;
110      cache_coherent= false;
111      is_cnt        = false;
112      dirty         = false;
113      lock          = false;
114      tag           = 0;
115      count         = 0;
116      owner.inst    = 0;
117      owner.srcid   = 0;
118#if L1_MULTI_CACHE
119      owner.cache_id= 0;
120#endif
121      ptr           = 0;
122    }
123
124    DirectoryEntry(const DirectoryEntry &source)
125    {
126      valid         = source.valid;
127      cache_coherent= source.cache_coherent;
128      is_cnt        = source.is_cnt;
129      dirty         = source.dirty;
130      lock          = source.lock;
131      tag           = source.tag;
132      count         = source.count;
133      owner         = source.owner;
134      ptr           = source.ptr;
135    }         
136
137    /////////////////////////////////////////////////////////////////////
138    // The init() function initializes the entry
139    /////////////////////////////////////////////////////////////////////
140    void init()
141    {
142      valid     = false;
143      cache_coherent = false;
144      is_cnt    = false;
145      dirty     = false;
146      lock      = false;
147      count     = 0;
148    }
149
150    /////////////////////////////////////////////////////////////////////
151    // The copy() function copies an existing source entry to a target
152    /////////////////////////////////////////////////////////////////////
153    void copy(const DirectoryEntry &source)
154    {
155      valid         = source.valid;
156      cache_coherent = source.cache_coherent;
157      is_cnt    = source.is_cnt;
158      dirty         = source.dirty;
159      lock          = source.lock;
160      tag           = source.tag;
161      count     = source.count;
162      owner     = source.owner;
163      ptr       = source.ptr;
164    }
165
166    ////////////////////////////////////////////////////////////////////
167    // The print() function prints the entry
168    ////////////////////////////////////////////////////////////////////
169    void print()
170    {
171      std::cout << "Valid = " << valid
172                << " ; COHERENCE = " << cache_coherent
173                << " ; IS COUNT = " << is_cnt
174                << " ; Dirty = " << dirty
175                << " ; Lock = " << lock
176                << " ; Tag = " << std::hex << tag << std::dec
177                << " ; Count = " << count
178                << " ; Owner = " << owner.srcid
179#if L1_MULTI_CACHE
180                << "." << owner.cache_id
181#endif
182                << " " << owner.inst
183                << " ; Pointer = " << ptr << std::endl;
184    }
185
186  }; // end class DirectoryEntry
187
188  ////////////////////////////////////////////////////////////////////////
189  //                       The directory 
190  ////////////////////////////////////////////////////////////////////////
191  class CacheDirectory {
192
193    typedef sc_dt::sc_uint<40> addr_t;
194    typedef uint32_t data_t;
195    typedef uint32_t tag_t;
196
197    private:
198
199    // Directory constants
200    size_t                                      m_ways;
201    size_t                                      m_sets;
202    size_t                                      m_words;
203    size_t                                      m_width;
204    uint32_t                lfsr;
205
206    // the directory & lru tables
207    DirectoryEntry                              **m_dir_tab;
208    LruEntry                                    **m_lru_tab;
209
210    public:
211
212    ////////////////////////
213    // Constructor
214    ////////////////////////
215    CacheDirectory( size_t ways, size_t sets, size_t words, size_t address_width)       
216    {
217      m_ways  = ways; 
218      m_sets  = sets;
219      m_words = words;
220      m_width = address_width;
221      lfsr = -1;
222
223      m_dir_tab = new DirectoryEntry*[sets];
224      for ( size_t i=0; i<sets; i++ ) {
225        m_dir_tab[i] = new DirectoryEntry[ways];
226        for ( size_t j=0 ; j<ways ; j++) m_dir_tab[i][j].init();
227      }
228      m_lru_tab = new LruEntry*[sets];
229      for ( size_t i=0; i<sets; i++ ) {
230        m_lru_tab[i] = new LruEntry[ways];
231        for ( size_t j=0 ; j<ways ; j++) m_lru_tab[i][j].init();
232      }
233    } // end constructor
234
235    /////////////////
236    // Destructor
237    /////////////////
238    ~CacheDirectory()
239    {
240      for(size_t i=0 ; i<m_sets ; i++){
241        delete [] m_dir_tab[i];
242        delete [] m_lru_tab[i];
243      }
244      delete [] m_dir_tab;
245      delete [] m_lru_tab;
246    } // end destructor
247
248    /////////////////////////////////////////////////////////////////////
249    // The read() function reads a directory entry. In case of hit, the
250    // LRU is updated.
251    // Arguments :
252    // - address : the address of the entry
253    // - way : (return argument) the way of the entry in case of hit
254    // The function returns a copy of a (valid or invalid) entry 
255    /////////////////////////////////////////////////////////////////////
256    DirectoryEntry read(const addr_t &address, size_t &way)
257    {
258
259#define L2 soclib::common::uint32_log2
260      const size_t set = (size_t)(address >> (L2(m_words) + 2)) & (m_sets - 1);
261      const tag_t  tag = (tag_t)(address >> (L2(m_sets) + L2(m_words) + 2));
262#undef L2
263
264      bool hit       = false;
265      for ( size_t i=0 ; i<m_ways ; i++ ) {
266        bool equal = ( m_dir_tab[set][i].tag == tag );
267        bool valid = m_dir_tab[set][i].valid;
268        hit = equal && valid;
269        if ( hit ) {                   
270          way = i;
271          break;
272        } 
273      }
274      if ( hit ) {
275        m_lru_tab[set][way].recent = true;
276        return DirectoryEntry(m_dir_tab[set][way]);
277      } else {
278        return DirectoryEntry();
279      }
280    } // end read()
281
282    /////////////////////////////////////////////////////////////////////
283    // The inval function invalidate an entry defined by the set and
284    // way arguments.
285    /////////////////////////////////////////////////////////////////////
286    void inval( const size_t &way, const size_t &set )
287    {
288        m_dir_tab[set][way].init();
289    }
290
291    /////////////////////////////////////////////////////////////////////
292    // The read_neutral() function reads a directory entry, without
293    // changing the LRU
294    // Arguments :
295    // - address : the address of the entry
296    // The function returns a copy of a (valid or invalid) entry 
297    /////////////////////////////////////////////////////////////////////
298    DirectoryEntry read_neutral( const addr_t &address, 
299                                 size_t*      ret_way,
300                                 size_t*      ret_set )
301    {
302
303#define L2 soclib::common::uint32_log2
304        size_t set = (size_t)(address >> (L2(m_words) + 2)) & (m_sets - 1);
305        tag_t  tag = (tag_t)(address >> (L2(m_sets) + L2(m_words) + 2));
306#undef L2
307
308        for ( size_t way = 0 ; way < m_ways ; way++ ) 
309        {
310            bool equal = ( m_dir_tab[set][way].tag == tag );
311            bool valid = m_dir_tab[set][way].valid;
312            if ( equal and valid )
313            {
314                *ret_set = set;
315                *ret_way = way; 
316                return DirectoryEntry(m_dir_tab[set][way]);
317            }
318        } 
319        return DirectoryEntry();
320    } // end read_neutral()
321
322    /////////////////////////////////////////////////////////////////////
323    // The write function writes a new entry,
324    // and updates the LRU bits if necessary.
325    // Arguments :
326    // - set : the set of the entry
327    // - way : the way of the entry
328    // - entry : the entry value
329    /////////////////////////////////////////////////////////////////////
330    void write(const size_t &set, const size_t &way, const DirectoryEntry &entry)
331    {
332      assert( (set<m_sets) 
333          && "Cache Directory write : The set index is invalid");
334      assert( (way<m_ways) 
335          && "Cache Directory write : The way index is invalid");
336
337      // update Directory
338      m_dir_tab[set][way].copy(entry);
339
340      // update LRU bits
341      bool all_recent = true;
342      for ( size_t i=0 ; i<m_ways ; i++ ) 
343      {
344          if ( i != way ) all_recent = m_lru_tab[set][i].recent && all_recent;
345      }
346      if ( all_recent ) 
347      {
348          for( size_t i=0 ; i<m_ways ; i++ ) m_lru_tab[set][i].recent = false;
349      } 
350      else 
351      {
352          m_lru_tab[set][way].recent = true;
353      }
354    } // end write()
355
356    /////////////////////////////////////////////////////////////////////
357    // The print() function prints a selected directory entry
358    // Arguments :
359    // - set : the set of the entry to print
360    // - way : the way of the entry to print
361    /////////////////////////////////////////////////////////////////////
362    void print(const size_t &set, const size_t &way)
363    {
364      std::cout << std::dec << " set : " << set << " ; way : " << way << " ; " ;
365      m_dir_tab[set][way].print();
366    } // end print()
367
368    /////////////////////////////////////////////////////////////////////
369    // The select() function selects a directory entry to evince.
370    // Arguments :
371    // - set   : (input argument) the set to modify
372    // - way   : (return argument) the way to evince
373    /////////////////////////////////////////////////////////////////////
374    DirectoryEntry select(const size_t &set, size_t &way)
375    {
376      assert( (set < m_sets) 
377          && "Cache Directory : (select) The set index is invalid");
378
379      for(size_t i=0; i<m_ways; i++){
380        if(!m_dir_tab[set][i].valid){
381          way=i;
382          return DirectoryEntry(m_dir_tab[set][way]);
383        }
384      }
385
386#ifdef RANDOM_EVICTION
387      lfsr = (lfsr >> 1) ^ ((-(lfsr & 1)) & 0xd0000001);
388      way = lfsr % m_ways;
389      return DirectoryEntry(m_dir_tab[set][way]);
390#endif
391
392      for(size_t i=0; i<m_ways; i++){
393        if(!(m_lru_tab[set][i].recent) && !(m_dir_tab[set][i].lock)){
394          way=i;
395          return DirectoryEntry(m_dir_tab[set][way]);
396        }
397      }
398      for(size_t i=0; i<m_ways; i++){
399        if( !(m_lru_tab[set][i].recent) && (m_dir_tab[set][i].lock)){
400          way=i;
401          return DirectoryEntry(m_dir_tab[set][way]);
402        }
403      }
404      for(size_t i=0; i<m_ways; i++){
405        if( (m_lru_tab[set][i].recent) && !(m_dir_tab[set][i].lock)){
406          way=i;
407          return DirectoryEntry(m_dir_tab[set][way]);
408        }
409      }
410      way = 0;
411      return DirectoryEntry(m_dir_tab[set][0]);
412    } // end select()
413
414    /////////////////////////////////////////////////////////////////////
415    //          Global initialisation function
416    /////////////////////////////////////////////////////////////////////
417    void init()
418    {
419      for ( size_t set=0 ; set<m_sets ; set++ ) 
420      {
421        for ( size_t way=0 ; way<m_ways ; way++ ) 
422        {
423          m_dir_tab[set][way].init();
424          m_lru_tab[set][way].init();
425        }
426      }
427    } // end init()
428
429  }; // end class CacheDirectory
430
431  ///////////////////////////////////////////////////////////////////////
432  //                    A Heap Entry
433  ///////////////////////////////////////////////////////////////////////
434  class HeapEntry{
435
436    public:
437    // Fields of the entry
438      Owner     owner;
439      size_t    next;
440
441    ////////////////////////
442    // Constructor
443    ////////////////////////
444      HeapEntry()
445      :owner(false,0
446#if L1_MULTI_CACHE
447             ,0
448#endif
449             )
450      {
451        next = 0;
452      } // end constructor
453
454    ////////////////////////
455    // Constructor
456    ////////////////////////
457      HeapEntry(const HeapEntry &entry){
458        owner.inst  = entry.owner.inst;
459        owner.srcid = entry.owner.srcid;
460#if L1_MULTI_CACHE
461        owner.cache_id = entry.owner.cache_id;
462#endif       
463        next           = entry.next;
464      } // end constructor
465
466    /////////////////////////////////////////////////////////////////////
467    // The copy() function copies an existing source entry to a target
468    /////////////////////////////////////////////////////////////////////
469      void copy(const HeapEntry &entry){
470        owner.inst     = entry.owner.inst;
471        owner.srcid    = entry.owner.srcid;
472#if L1_MULTI_CACHE
473        owner.cache_id = entry.owner.cache_id;
474#endif
475        next           = entry.next;
476      } // end copy()
477
478    ////////////////////////////////////////////////////////////////////
479    // The print() function prints the entry
480    ////////////////////////////////////////////////////////////////////
481      void print(){
482        std::cout
483        << " -- owner.inst     : " << std::dec << owner.inst << std::endl
484        << " -- owner.srcid    : " << std::dec << owner.srcid << std::endl
485#if L1_MULTI_CACHE
486        << " -- owner.cache_id : " << std::dec << owner.cache_id << std::endl
487#endif
488        << " -- next           : " << std::dec << next << std::endl;
489
490      } // end print()
491
492  }; // end class HeapEntry
493
494  ////////////////////////////////////////////////////////////////////////
495  //                        The Heap
496  ////////////////////////////////////////////////////////////////////////
497  class HeapDirectory{
498   
499    private:
500    // Registers and the heap
501      size_t    ptr_free;
502      bool      full;
503      HeapEntry *m_heap_tab;
504
505    // Constants for debugging purpose
506      size_t    tab_size;
507
508    public:
509    ////////////////////////
510    // Constructor
511    ////////////////////////
512      HeapDirectory(uint32_t size){
513        assert(size>0 && "Memory Cache, HeapDirectory constructor : invalid size");
514        ptr_free    = 0;
515        full        = false;
516        m_heap_tab  = new HeapEntry[size];
517        tab_size    = size;
518      } // end constructor
519
520    /////////////////
521    // Destructor
522    /////////////////
523      ~HeapDirectory(){
524        delete [] m_heap_tab;
525      } // end destructor
526
527    /////////////////////////////////////////////////////////////////////
528    //          Global initialisation function
529    /////////////////////////////////////////////////////////////////////
530      void init(){
531        ptr_free=0;
532        full=false;
533        for(size_t i=0; i< tab_size-1;i++){
534          m_heap_tab[i].next = i+1;
535        }
536        m_heap_tab[tab_size-1].next = tab_size-1;
537        return;
538      }
539
540    /////////////////////////////////////////////////////////////////////
541    // The print() function prints a selected directory entry
542    // Arguments :
543    // - ptr : the pointer to the entry to print
544    /////////////////////////////////////////////////////////////////////
545      void print(const size_t &ptr){
546        std::cout << "Heap, printing the entry : " << std::dec << ptr << std::endl;
547        m_heap_tab[ptr].print();
548      } // end print()
549
550    /////////////////////////////////////////////////////////////////////
551    // The print_list() function prints a list from selected directory entry
552    // Arguments :
553    // - ptr : the pointer to the first entry to print
554    /////////////////////////////////////////////////////////////////////
555      void print_list(const size_t &ptr){
556        bool end = false;
557        size_t ptr_temp = ptr;
558        std::cout << "Heap, printing the list from : " << std::dec << ptr << std::endl;
559        while(!end){
560            m_heap_tab[ptr_temp].print();
561            if(ptr_temp == m_heap_tab[ptr_temp].next) end = true;
562            ptr_temp = m_heap_tab[ptr_temp].next;
563        } 
564      } // end print_list()
565
566    /////////////////////////////////////////////////////////////////////
567    // The is_full() function return true if the heap is full.
568    /////////////////////////////////////////////////////////////////////
569      bool is_full(){
570        return full;
571      } // end is_full()
572
573    /////////////////////////////////////////////////////////////////////
574    // The next_free_ptr() function returns the pointer
575    // to the next free entry.
576    /////////////////////////////////////////////////////////////////////
577      size_t next_free_ptr(){
578        return ptr_free;
579      } // end next_free_ptr()
580
581    /////////////////////////////////////////////////////////////////////
582    // The next_free_entry() function returns
583    // a copy of the next free entry.
584    /////////////////////////////////////////////////////////////////////
585      HeapEntry next_free_entry(){
586        return HeapEntry(m_heap_tab[ptr_free]);
587      } // end next_free_entry()
588   
589    /////////////////////////////////////////////////////////////////////
590    // The write_free_entry() function modify the next free entry.
591    // Arguments :
592    // - entry : the entry to write
593    /////////////////////////////////////////////////////////////////////
594      void write_free_entry(const HeapEntry &entry){
595        m_heap_tab[ptr_free].copy(entry);
596      } // end write_free_entry()
597
598    /////////////////////////////////////////////////////////////////////
599    // The write_free_ptr() function writes the pointer
600    // to the next free entry
601    /////////////////////////////////////////////////////////////////////
602      void write_free_ptr(const size_t &ptr){
603        assert( (ptr<tab_size) && "HeapDirectory error : try to write a wrong free pointer");
604        ptr_free = ptr;
605      } // end write_free_ptr()
606
607    /////////////////////////////////////////////////////////////////////
608    // The set_full() function sets the full bit (to true).
609    /////////////////////////////////////////////////////////////////////
610      void set_full(){
611        full = true;
612      } // end set_full()
613
614    /////////////////////////////////////////////////////////////////////
615    // The unset_full() function unsets the full bit (to false).
616    /////////////////////////////////////////////////////////////////////
617      void unset_full(){
618        full = false;
619      } // end unset_full()
620
621    /////////////////////////////////////////////////////////////////////
622    // The read() function returns a copy of
623    // the entry pointed by the argument
624    // Arguments :
625    //  - ptr : the pointer to the entry to read
626    /////////////////////////////////////////////////////////////////////
627      HeapEntry read(const size_t &ptr){
628        assert( (ptr<tab_size) && "HeapDirectory error : try to write a wrong free pointer");
629        return HeapEntry(m_heap_tab[ptr]);
630      } // end read()
631
632    /////////////////////////////////////////////////////////////////////
633    // The write() function writes an entry in the heap
634    // Arguments :
635    //  - ptr : the pointer to the entry to replace
636    //  - entry : the entry to write
637    /////////////////////////////////////////////////////////////////////
638      void write(const size_t &ptr, const HeapEntry &entry){
639        assert( (ptr<tab_size) && "HeapDirectory error : try to write a wrong free pointer");
640        m_heap_tab[ptr].copy(entry);
641      } // end write()
642
643  }; // end class HeapDirectory
644
645  ////////////////////////////////////////////////////////////////////////
646  //                        Cache Data
647  ////////////////////////////////////////////////////////////////////////
648  class CacheData {
649    private:
650      const uint32_t m_sets;
651      const uint32_t m_ways;
652      const uint32_t m_words;
653
654      uint32_t *** m_cache_data;
655
656    public:
657
658      CacheData(uint32_t ways, uint32_t sets, uint32_t words)
659        : m_sets(sets), m_ways(ways), m_words(words) {
660
661          m_cache_data = new uint32_t ** [ways];
662          for ( size_t i=0 ; i < ways ; i++ ) {
663            m_cache_data[i] = new uint32_t * [sets];
664          }
665          for ( size_t i=0; i<ways; i++ ) {
666            for ( size_t j=0; j<sets; j++ ) {
667              m_cache_data[i][j] = new uint32_t [words];
668            }
669          }
670        }
671
672      ~CacheData() {
673          for(size_t i=0; i<m_ways ; i++){
674              for(size_t j=0; j<m_sets ; j++){
675                  delete [] m_cache_data[i][j];
676              }
677          }
678          for(size_t i=0; i<m_ways ; i++){
679              delete [] m_cache_data[i];
680          }
681          delete [] m_cache_data;
682      }
683
684      uint32_t read (
685          const uint32_t &way,
686          const uint32_t &set,
687          const uint32_t &word) const {
688
689        assert((set  < m_sets ) && "Cache data error: Trying to read a wrong set" );
690        assert((way  < m_ways ) && "Cache data error: Trying to read a wrong way" );
691        assert((word < m_words) && "Cache data error: Trying to read a wrong word");
692
693        return m_cache_data[way][set][word];
694      }
695
696      void read_line(
697          const uint32_t &way,
698          const uint32_t &set,
699          sc_core::sc_signal<uint32_t> * cache_line)
700      {
701        assert((set < m_sets ) && "Cache data error: Trying to read a wrong set" );
702        assert((way < m_ways ) && "Cache data error: Trying to read a wrong way" );
703
704        for (uint32_t word=0; word<m_words; word++)
705          cache_line[word].write(m_cache_data[way][set][word]);
706      }
707
708      void write (
709          const uint32_t &way,
710          const uint32_t &set,
711          const uint32_t &word,
712          const uint32_t &data,
713          const uint32_t &be = 0xF) {
714
715        assert((set  < m_sets ) && "Cache data error: Trying to write a wrong set" );
716        assert((way  < m_ways ) && "Cache data error: Trying to write a wrong way" );
717        assert((word < m_words) && "Cache data error: Trying to write a wrong word");
718        assert((be  <= 0xF    ) && "Cache data error: Trying to write a wrong word cell");
719
720        if (be == 0x0) return;
721
722        if (be == 0xF) {
723            m_cache_data[way][set][word] = data; 
724            return;
725        }
726
727        uint32_t mask = 0;
728        if  (be & 0x1) mask = mask | 0x000000FF;
729        if  (be & 0x2) mask = mask | 0x0000FF00;
730        if  (be & 0x4) mask = mask | 0x00FF0000;
731        if  (be & 0x8) mask = mask | 0xFF000000;
732
733        m_cache_data[way][set][word] = 
734          (data & mask) | (m_cache_data[way][set][word] & ~mask);
735      }
736  }; // end class CacheData
737
738}} // end namespaces
739
740#endif
741
742// Local Variables:
743// tab-width: 4
744// c-basic-offset: 4
745// c-file-offsets:((innamespace . 0)(inline-open . 0))
746// indent-tabs-mode: nil
747// End:
748
749// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=4:softtabstop=4
750
Note: See TracBrowser for help on using the repository browser.