qemu/hw/tcx.c
<<
>>
Prefs
   1/*
   2 * QEMU TCX Frame buffer
   3 *
   4 * Copyright (c) 2003-2005 Fabrice Bellard
   5 *
   6 * Permission is hereby granted, free of charge, to any person obtaining a copy
   7 * of this software and associated documentation files (the "Software"), to deal
   8 * in the Software without restriction, including without limitation the rights
   9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10 * copies of the Software, and to permit persons to whom the Software is
  11 * furnished to do so, subject to the following conditions:
  12 *
  13 * The above copyright notice and this permission notice shall be included in
  14 * all copies or substantial portions of the Software.
  15 *
  16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
  19 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  22 * THE SOFTWARE.
  23 */
  24
  25#include "console.h"
  26#include "pixel_ops.h"
  27#include "sysbus.h"
  28#include "qdev-addr.h"
  29
  30#define MAXX 1024
  31#define MAXY 768
  32#define TCX_DAC_NREGS 16
  33#define TCX_THC_NREGS_8  0x081c
  34#define TCX_THC_NREGS_24 0x1000
  35#define TCX_TEC_NREGS    0x1000
  36
  37typedef struct TCXState {
  38    SysBusDevice busdev;
  39    target_phys_addr_t addr;
  40    DisplayState *ds;
  41    uint8_t *vram;
  42    uint32_t *vram24, *cplane;
  43    MemoryRegion vram_mem;
  44    MemoryRegion vram_8bit;
  45    MemoryRegion vram_24bit;
  46    MemoryRegion vram_cplane;
  47    MemoryRegion dac;
  48    MemoryRegion tec;
  49    MemoryRegion thc24;
  50    MemoryRegion thc8;
  51    ram_addr_t vram24_offset, cplane_offset;
  52    uint32_t vram_size;
  53    uint32_t palette[256];
  54    uint8_t r[256], g[256], b[256];
  55    uint16_t width, height, depth;
  56    uint8_t dac_index, dac_state;
  57} TCXState;
  58
  59static void tcx_screen_dump(void *opaque, const char *filename);
  60static void tcx24_screen_dump(void *opaque, const char *filename);
  61
  62static void tcx_set_dirty(TCXState *s)
  63{
  64    unsigned int i;
  65
  66    for (i = 0; i < MAXX * MAXY; i += TARGET_PAGE_SIZE) {
  67        memory_region_set_dirty(&s->vram_mem, i);
  68    }
  69}
  70
  71static void tcx24_set_dirty(TCXState *s)
  72{
  73    unsigned int i;
  74
  75    for (i = 0; i < MAXX * MAXY * 4; i += TARGET_PAGE_SIZE) {
  76        memory_region_set_dirty(&s->vram_mem, s->vram24_offset + i);
  77        memory_region_set_dirty(&s->vram_mem, s->cplane_offset + i);
  78    }
  79}
  80
  81static void update_palette_entries(TCXState *s, int start, int end)
  82{
  83    int i;
  84    for(i = start; i < end; i++) {
  85        switch(ds_get_bits_per_pixel(s->ds)) {
  86        default:
  87        case 8:
  88            s->palette[i] = rgb_to_pixel8(s->r[i], s->g[i], s->b[i]);
  89            break;
  90        case 15:
  91            s->palette[i] = rgb_to_pixel15(s->r[i], s->g[i], s->b[i]);
  92            break;
  93        case 16:
  94            s->palette[i] = rgb_to_pixel16(s->r[i], s->g[i], s->b[i]);
  95            break;
  96        case 32:
  97            if (is_surface_bgr(s->ds->surface))
  98                s->palette[i] = rgb_to_pixel32bgr(s->r[i], s->g[i], s->b[i]);
  99            else
 100                s->palette[i] = rgb_to_pixel32(s->r[i], s->g[i], s->b[i]);
 101            break;
 102        }
 103    }
 104    if (s->depth == 24) {
 105        tcx24_set_dirty(s);
 106    } else {
 107        tcx_set_dirty(s);
 108    }
 109}
 110
 111static void tcx_draw_line32(TCXState *s1, uint8_t *d,
 112                            const uint8_t *s, int width)
 113{
 114    int x;
 115    uint8_t val;
 116    uint32_t *p = (uint32_t *)d;
 117
 118    for(x = 0; x < width; x++) {
 119        val = *s++;
 120        *p++ = s1->palette[val];
 121    }
 122}
 123
 124static void tcx_draw_line16(TCXState *s1, uint8_t *d,
 125                            const uint8_t *s, int width)
 126{
 127    int x;
 128    uint8_t val;
 129    uint16_t *p = (uint16_t *)d;
 130
 131    for(x = 0; x < width; x++) {
 132        val = *s++;
 133        *p++ = s1->palette[val];
 134    }
 135}
 136
 137static void tcx_draw_line8(TCXState *s1, uint8_t *d,
 138                           const uint8_t *s, int width)
 139{
 140    int x;
 141    uint8_t val;
 142
 143    for(x = 0; x < width; x++) {
 144        val = *s++;
 145        *d++ = s1->palette[val];
 146    }
 147}
 148
 149/*
 150  XXX Could be much more optimal:
 151  * detect if line/page/whole screen is in 24 bit mode
 152  * if destination is also BGR, use memcpy
 153  */
 154static inline void tcx24_draw_line32(TCXState *s1, uint8_t *d,
 155                                     const uint8_t *s, int width,
 156                                     const uint32_t *cplane,
 157                                     const uint32_t *s24)
 158{
 159    int x, bgr, r, g, b;
 160    uint8_t val, *p8;
 161    uint32_t *p = (uint32_t *)d;
 162    uint32_t dval;
 163
 164    bgr = is_surface_bgr(s1->ds->surface);
 165    for(x = 0; x < width; x++, s++, s24++) {
 166        if ((be32_to_cpu(*cplane++) & 0xff000000) == 0x03000000) {
 167            // 24-bit direct, BGR order
 168            p8 = (uint8_t *)s24;
 169            p8++;
 170            b = *p8++;
 171            g = *p8++;
 172            r = *p8;
 173            if (bgr)
 174                dval = rgb_to_pixel32bgr(r, g, b);
 175            else
 176                dval = rgb_to_pixel32(r, g, b);
 177        } else {
 178            val = *s;
 179            dval = s1->palette[val];
 180        }
 181        *p++ = dval;
 182    }
 183}
 184
 185static inline int check_dirty(TCXState *s, ram_addr_t page, ram_addr_t page24,
 186                              ram_addr_t cpage)
 187{
 188    int ret;
 189    unsigned int off;
 190
 191    ret = memory_region_get_dirty(&s->vram_mem, page, DIRTY_MEMORY_VGA);
 192    for (off = 0; off < TARGET_PAGE_SIZE * 4; off += TARGET_PAGE_SIZE) {
 193        ret |= memory_region_get_dirty(&s->vram_mem, page24 + off,
 194                                       DIRTY_MEMORY_VGA);
 195        ret |= memory_region_get_dirty(&s->vram_mem, cpage + off,
 196                                       DIRTY_MEMORY_VGA);
 197    }
 198    return ret;
 199}
 200
 201static inline void reset_dirty(TCXState *ts, ram_addr_t page_min,
 202                               ram_addr_t page_max, ram_addr_t page24,
 203                              ram_addr_t cpage)
 204{
 205    memory_region_reset_dirty(&ts->vram_mem,
 206                              page_min, page_max + TARGET_PAGE_SIZE,
 207                              DIRTY_MEMORY_VGA);
 208    memory_region_reset_dirty(&ts->vram_mem,
 209                              page24 + page_min * 4,
 210                              page24 + page_max * 4 + TARGET_PAGE_SIZE,
 211                              DIRTY_MEMORY_VGA);
 212    memory_region_reset_dirty(&ts->vram_mem,
 213                              cpage + page_min * 4,
 214                              cpage + page_max * 4 + TARGET_PAGE_SIZE,
 215                              DIRTY_MEMORY_VGA);
 216}
 217
 218/* Fixed line length 1024 allows us to do nice tricks not possible on
 219   VGA... */
 220static void tcx_update_display(void *opaque)
 221{
 222    TCXState *ts = opaque;
 223    ram_addr_t page, page_min, page_max;
 224    int y, y_start, dd, ds;
 225    uint8_t *d, *s;
 226    void (*f)(TCXState *s1, uint8_t *dst, const uint8_t *src, int width);
 227
 228    if (ds_get_bits_per_pixel(ts->ds) == 0)
 229        return;
 230    page = 0;
 231    y_start = -1;
 232    page_min = -1;
 233    page_max = 0;
 234    d = ds_get_data(ts->ds);
 235    s = ts->vram;
 236    dd = ds_get_linesize(ts->ds);
 237    ds = 1024;
 238
 239    switch (ds_get_bits_per_pixel(ts->ds)) {
 240    case 32:
 241        f = tcx_draw_line32;
 242        break;
 243    case 15:
 244    case 16:
 245        f = tcx_draw_line16;
 246        break;
 247    default:
 248    case 8:
 249        f = tcx_draw_line8;
 250        break;
 251    case 0:
 252        return;
 253    }
 254
 255    for(y = 0; y < ts->height; y += 4, page += TARGET_PAGE_SIZE) {
 256        if (memory_region_get_dirty(&ts->vram_mem, page, DIRTY_MEMORY_VGA)) {
 257            if (y_start < 0)
 258                y_start = y;
 259            if (page < page_min)
 260                page_min = page;
 261            if (page > page_max)
 262                page_max = page;
 263            f(ts, d, s, ts->width);
 264            d += dd;
 265            s += ds;
 266            f(ts, d, s, ts->width);
 267            d += dd;
 268            s += ds;
 269            f(ts, d, s, ts->width);
 270            d += dd;
 271            s += ds;
 272            f(ts, d, s, ts->width);
 273            d += dd;
 274            s += ds;
 275        } else {
 276            if (y_start >= 0) {
 277                /* flush to display */
 278                dpy_update(ts->ds, 0, y_start,
 279                           ts->width, y - y_start);
 280                y_start = -1;
 281            }
 282            d += dd * 4;
 283            s += ds * 4;
 284        }
 285    }
 286    if (y_start >= 0) {
 287        /* flush to display */
 288        dpy_update(ts->ds, 0, y_start,
 289                   ts->width, y - y_start);
 290    }
 291    /* reset modified pages */
 292    if (page_max >= page_min) {
 293        memory_region_reset_dirty(&ts->vram_mem,
 294                                  page_min, page_max + TARGET_PAGE_SIZE,
 295                                  DIRTY_MEMORY_VGA);
 296    }
 297}
 298
 299static void tcx24_update_display(void *opaque)
 300{
 301    TCXState *ts = opaque;
 302    ram_addr_t page, page_min, page_max, cpage, page24;
 303    int y, y_start, dd, ds;
 304    uint8_t *d, *s;
 305    uint32_t *cptr, *s24;
 306
 307    if (ds_get_bits_per_pixel(ts->ds) != 32)
 308            return;
 309    page = 0;
 310    page24 = ts->vram24_offset;
 311    cpage = ts->cplane_offset;
 312    y_start = -1;
 313    page_min = -1;
 314    page_max = 0;
 315    d = ds_get_data(ts->ds);
 316    s = ts->vram;
 317    s24 = ts->vram24;
 318    cptr = ts->cplane;
 319    dd = ds_get_linesize(ts->ds);
 320    ds = 1024;
 321
 322    for(y = 0; y < ts->height; y += 4, page += TARGET_PAGE_SIZE,
 323            page24 += TARGET_PAGE_SIZE, cpage += TARGET_PAGE_SIZE) {
 324        if (check_dirty(ts, page, page24, cpage)) {
 325            if (y_start < 0)
 326                y_start = y;
 327            if (page < page_min)
 328                page_min = page;
 329            if (page > page_max)
 330                page_max = page;
 331            tcx24_draw_line32(ts, d, s, ts->width, cptr, s24);
 332            d += dd;
 333            s += ds;
 334            cptr += ds;
 335            s24 += ds;
 336            tcx24_draw_line32(ts, d, s, ts->width, cptr, s24);
 337            d += dd;
 338            s += ds;
 339            cptr += ds;
 340            s24 += ds;
 341            tcx24_draw_line32(ts, d, s, ts->width, cptr, s24);
 342            d += dd;
 343            s += ds;
 344            cptr += ds;
 345            s24 += ds;
 346            tcx24_draw_line32(ts, d, s, ts->width, cptr, s24);
 347            d += dd;
 348            s += ds;
 349            cptr += ds;
 350            s24 += ds;
 351        } else {
 352            if (y_start >= 0) {
 353                /* flush to display */
 354                dpy_update(ts->ds, 0, y_start,
 355                           ts->width, y - y_start);
 356                y_start = -1;
 357            }
 358            d += dd * 4;
 359            s += ds * 4;
 360            cptr += ds * 4;
 361            s24 += ds * 4;
 362        }
 363    }
 364    if (y_start >= 0) {
 365        /* flush to display */
 366        dpy_update(ts->ds, 0, y_start,
 367                   ts->width, y - y_start);
 368    }
 369    /* reset modified pages */
 370    if (page_max >= page_min) {
 371        reset_dirty(ts, page_min, page_max, page24, cpage);
 372    }
 373}
 374
 375static void tcx_invalidate_display(void *opaque)
 376{
 377    TCXState *s = opaque;
 378
 379    tcx_set_dirty(s);
 380    qemu_console_resize(s->ds, s->width, s->height);
 381}
 382
 383static void tcx24_invalidate_display(void *opaque)
 384{
 385    TCXState *s = opaque;
 386
 387    tcx_set_dirty(s);
 388    tcx24_set_dirty(s);
 389    qemu_console_resize(s->ds, s->width, s->height);
 390}
 391
 392static int vmstate_tcx_post_load(void *opaque, int version_id)
 393{
 394    TCXState *s = opaque;
 395
 396    update_palette_entries(s, 0, 256);
 397    if (s->depth == 24) {
 398        tcx24_set_dirty(s);
 399    } else {
 400        tcx_set_dirty(s);
 401    }
 402
 403    return 0;
 404}
 405
 406static const VMStateDescription vmstate_tcx = {
 407    .name ="tcx",
 408    .version_id = 4,
 409    .minimum_version_id = 4,
 410    .minimum_version_id_old = 4,
 411    .post_load = vmstate_tcx_post_load,
 412    .fields      = (VMStateField []) {
 413        VMSTATE_UINT16(height, TCXState),
 414        VMSTATE_UINT16(width, TCXState),
 415        VMSTATE_UINT16(depth, TCXState),
 416        VMSTATE_BUFFER(r, TCXState),
 417        VMSTATE_BUFFER(g, TCXState),
 418        VMSTATE_BUFFER(b, TCXState),
 419        VMSTATE_UINT8(dac_index, TCXState),
 420        VMSTATE_UINT8(dac_state, TCXState),
 421        VMSTATE_END_OF_LIST()
 422    }
 423};
 424
 425static void tcx_reset(DeviceState *d)
 426{
 427    TCXState *s = container_of(d, TCXState, busdev.qdev);
 428
 429    /* Initialize palette */
 430    memset(s->r, 0, 256);
 431    memset(s->g, 0, 256);
 432    memset(s->b, 0, 256);
 433    s->r[255] = s->g[255] = s->b[255] = 255;
 434    update_palette_entries(s, 0, 256);
 435    memset(s->vram, 0, MAXX*MAXY);
 436    memory_region_reset_dirty(&s->vram_mem, 0, MAXX * MAXY * (1 + 4 + 4),
 437                              DIRTY_MEMORY_VGA);
 438    s->dac_index = 0;
 439    s->dac_state = 0;
 440}
 441
 442static uint64_t tcx_dac_readl(void *opaque, target_phys_addr_t addr,
 443                              unsigned size)
 444{
 445    return 0;
 446}
 447
 448static void tcx_dac_writel(void *opaque, target_phys_addr_t addr, uint64_t val,
 449                           unsigned size)
 450{
 451    TCXState *s = opaque;
 452
 453    switch (addr) {
 454    case 0:
 455        s->dac_index = val >> 24;
 456        s->dac_state = 0;
 457        break;
 458    case 4:
 459        switch (s->dac_state) {
 460        case 0:
 461            s->r[s->dac_index] = val >> 24;
 462            update_palette_entries(s, s->dac_index, s->dac_index + 1);
 463            s->dac_state++;
 464            break;
 465        case 1:
 466            s->g[s->dac_index] = val >> 24;
 467            update_palette_entries(s, s->dac_index, s->dac_index + 1);
 468            s->dac_state++;
 469            break;
 470        case 2:
 471            s->b[s->dac_index] = val >> 24;
 472            update_palette_entries(s, s->dac_index, s->dac_index + 1);
 473            s->dac_index = (s->dac_index + 1) & 255; // Index autoincrement
 474        default:
 475            s->dac_state = 0;
 476            break;
 477        }
 478        break;
 479    default:
 480        break;
 481    }
 482    return;
 483}
 484
 485static const MemoryRegionOps tcx_dac_ops = {
 486    .read = tcx_dac_readl,
 487    .write = tcx_dac_writel,
 488    .endianness = DEVICE_NATIVE_ENDIAN,
 489    .valid = {
 490        .min_access_size = 4,
 491        .max_access_size = 4,
 492    },
 493};
 494
 495static uint64_t dummy_readl(void *opaque, target_phys_addr_t addr,
 496                            unsigned size)
 497{
 498    return 0;
 499}
 500
 501static void dummy_writel(void *opaque, target_phys_addr_t addr,
 502                         uint64_t val, unsigned size)
 503{
 504}
 505
 506static const MemoryRegionOps dummy_ops = {
 507    .read = dummy_readl,
 508    .write = dummy_writel,
 509    .endianness = DEVICE_NATIVE_ENDIAN,
 510    .valid = {
 511        .min_access_size = 4,
 512        .max_access_size = 4,
 513    },
 514};
 515
 516static int tcx_init1(SysBusDevice *dev)
 517{
 518    TCXState *s = FROM_SYSBUS(TCXState, dev);
 519    ram_addr_t vram_offset = 0;
 520    int size;
 521    uint8_t *vram_base;
 522
 523    memory_region_init_ram(&s->vram_mem, NULL, "tcx.vram",
 524                           s->vram_size * (1 + 4 + 4));
 525    vram_base = memory_region_get_ram_ptr(&s->vram_mem);
 526
 527    /* 8-bit plane */
 528    s->vram = vram_base;
 529    size = s->vram_size;
 530    memory_region_init_alias(&s->vram_8bit, "tcx.vram.8bit",
 531                             &s->vram_mem, vram_offset, size);
 532    sysbus_init_mmio_region(dev, &s->vram_8bit);
 533    vram_offset += size;
 534    vram_base += size;
 535
 536    /* DAC */
 537    memory_region_init_io(&s->dac, &tcx_dac_ops, s, "tcx.dac", TCX_DAC_NREGS);
 538    sysbus_init_mmio_region(dev, &s->dac);
 539
 540    /* TEC (dummy) */
 541    memory_region_init_io(&s->tec, &dummy_ops, s, "tcx.tec", TCX_TEC_NREGS);
 542    sysbus_init_mmio_region(dev, &s->tec);
 543    /* THC: NetBSD writes here even with 8-bit display: dummy */
 544    memory_region_init_io(&s->thc24, &dummy_ops, s, "tcx.thc24",
 545                          TCX_THC_NREGS_24);
 546    sysbus_init_mmio_region(dev, &s->thc24);
 547
 548    if (s->depth == 24) {
 549        /* 24-bit plane */
 550        size = s->vram_size * 4;
 551        s->vram24 = (uint32_t *)vram_base;
 552        s->vram24_offset = vram_offset;
 553        memory_region_init_alias(&s->vram_24bit, "tcx.vram.24bit",
 554                                 &s->vram_mem, vram_offset, size);
 555        sysbus_init_mmio_region(dev, &s->vram_24bit);
 556        vram_offset += size;
 557        vram_base += size;
 558
 559        /* Control plane */
 560        size = s->vram_size * 4;
 561        s->cplane = (uint32_t *)vram_base;
 562        s->cplane_offset = vram_offset;
 563        memory_region_init_alias(&s->vram_cplane, "tcx.vram.cplane",
 564                                 &s->vram_mem, vram_offset, size);
 565        sysbus_init_mmio_region(dev, &s->vram_cplane);
 566
 567        s->ds = graphic_console_init(tcx24_update_display,
 568                                     tcx24_invalidate_display,
 569                                     tcx24_screen_dump, NULL, s);
 570    } else {
 571        /* THC 8 bit (dummy) */
 572        memory_region_init_io(&s->thc8, &dummy_ops, s, "tcx.thc8",
 573                              TCX_THC_NREGS_8);
 574        sysbus_init_mmio_region(dev, &s->thc8);
 575
 576        s->ds = graphic_console_init(tcx_update_display,
 577                                     tcx_invalidate_display,
 578                                     tcx_screen_dump, NULL, s);
 579    }
 580
 581    qemu_console_resize(s->ds, s->width, s->height);
 582    return 0;
 583}
 584
 585static void tcx_screen_dump(void *opaque, const char *filename)
 586{
 587    TCXState *s = opaque;
 588    FILE *f;
 589    uint8_t *d, *d1, v;
 590    int y, x;
 591
 592    f = fopen(filename, "wb");
 593    if (!f)
 594        return;
 595    fprintf(f, "P6\n%d %d\n%d\n", s->width, s->height, 255);
 596    d1 = s->vram;
 597    for(y = 0; y < s->height; y++) {
 598        d = d1;
 599        for(x = 0; x < s->width; x++) {
 600            v = *d;
 601            fputc(s->r[v], f);
 602            fputc(s->g[v], f);
 603            fputc(s->b[v], f);
 604            d++;
 605        }
 606        d1 += MAXX;
 607    }
 608    fclose(f);
 609    return;
 610}
 611
 612static void tcx24_screen_dump(void *opaque, const char *filename)
 613{
 614    TCXState *s = opaque;
 615    FILE *f;
 616    uint8_t *d, *d1, v;
 617    uint32_t *s24, *cptr, dval;
 618    int y, x;
 619
 620    f = fopen(filename, "wb");
 621    if (!f)
 622        return;
 623    fprintf(f, "P6\n%d %d\n%d\n", s->width, s->height, 255);
 624    d1 = s->vram;
 625    s24 = s->vram24;
 626    cptr = s->cplane;
 627    for(y = 0; y < s->height; y++) {
 628        d = d1;
 629        for(x = 0; x < s->width; x++, d++, s24++) {
 630            if ((*cptr++ & 0xff000000) == 0x03000000) { // 24-bit direct
 631                dval = *s24 & 0x00ffffff;
 632                fputc((dval >> 16) & 0xff, f);
 633                fputc((dval >> 8) & 0xff, f);
 634                fputc(dval & 0xff, f);
 635            } else {
 636                v = *d;
 637                fputc(s->r[v], f);
 638                fputc(s->g[v], f);
 639                fputc(s->b[v], f);
 640            }
 641        }
 642        d1 += MAXX;
 643    }
 644    fclose(f);
 645    return;
 646}
 647
 648static SysBusDeviceInfo tcx_info = {
 649    .init = tcx_init1,
 650    .qdev.name  = "SUNW,tcx",
 651    .qdev.size  = sizeof(TCXState),
 652    .qdev.reset = tcx_reset,
 653    .qdev.vmsd  = &vmstate_tcx,
 654    .qdev.props = (Property[]) {
 655        DEFINE_PROP_TADDR("addr",      TCXState, addr,      -1),
 656        DEFINE_PROP_HEX32("vram_size", TCXState, vram_size, -1),
 657        DEFINE_PROP_UINT16("width",    TCXState, width,     -1),
 658        DEFINE_PROP_UINT16("height",   TCXState, height,    -1),
 659        DEFINE_PROP_UINT16("depth",    TCXState, depth,     -1),
 660        DEFINE_PROP_END_OF_LIST(),
 661    }
 662};
 663
 664static void tcx_register_devices(void)
 665{
 666    sysbus_register_withprop(&tcx_info);
 667}
 668
 669device_init(tcx_register_devices)
 670